forge-guardrails 0.1.2

Foundation types for an LLM-agent workflow framework
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
# forge-guardrails

[![Rust](https://img.shields.io/badge/rust-1.95%2B-orange.svg)](https://www.rust-lang.org/)
[![Crates.io](https://img.shields.io/crates/v/forge-guardrails.svg)](https://crates.io/crates/forge-guardrails)
[![CI](https://github.com/whit3rabbit/forge-guardrails/actions/workflows/ci.yml/badge.svg)](https://github.com/whit3rabbit/forge-guardrails/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)

A Rust implementation of [`antoinezambelli/forge`](https://github.com/antoinezambelli/forge). See python version for original.

This was mostly a test of my clean-room-skill repo to see if it could manage to reproduce in rust. It somewhat succeeded, but I made a lot of tweaks to more closely match the original project. 

# Summary

A reliability layer for self-hosted LLM tool-calling. You give forge a set of tools; the model calls whichever it wants in whatever order. Workflow structure is opt-in — `required_steps`, `prerequisites`, and `terminal_tool` let you constrain the loop when you need to, but forge's guardrails (rescue parsing, retry nudges, response validation) apply with zero required steps too.

**What forge-guardrails isn't:**
- **Not an agent orchestrator.** Forge sits inside one agentic loop and makes its tool calls reliable. Multi-agent graphs, DAG planners, and cross-agent coordination are out of scope.
- **Not a coding harness.** Forge is domain-agnostic. If you're building a coding agent, [proxy mode]#proxy-server lifts your existing harness with forge's guardrails — no rewrite.

**Three ways to use it:**

- **Proxy server** — Drop-in OpenAI-compatible and Anthropic-compatible proxy (`forge-guardrails-proxy` binary) that sits between any client and a local model server. Applies guardrails transparently. Also accepts Anthropic Messages API requests at `POST /v1/messages`, translated through `anyllm_translate`.

- **WorkflowRunner** — Define tools, pick a backend, run structured agent loops. Forge manages the full lifecycle: system prompts, tool execution, context compaction, and guardrails. **SlotWorker** adds priority-queued access to a shared inference slot with auto-preemption — for multi-agent architectures where specialist workflows share a GPU slot. Best when you're building on forge directly.

- **Guardrails middleware** — Use forge's reliability stack inside your own orchestration loop. You control the loop; forge validates responses, rescues malformed tool calls, and enforces required steps.

Supports Ollama, llama-server (llama.cpp), Llamafile, Anthropic, and anyllm-routed OpenAI-compatible upstreams as backends.

> Status: experimental. Behavioral parity with the Python reference has been verified through the parity test suite. Review for production hardening before deployment — see [Known review areas]#known-review-areas-before-release.

## Provenance

- Original project: [`antoinezambelli/forge`]https://github.com/antoinezambelli/forge

Release notes live in [CHANGELOG.md](CHANGELOG.md).

## Requirements

- Rust 1.95+
- A running LLM backend (see below)

## Install

Install the proxy binary:

```bash
# macOS, using the Homebrew cask
brew install --cask whit3rabbit/tap/forge-guardrails-proxy

# macOS or Linux, from crates.io
cargo install forge-guardrails --locked --bin forge-guardrails-proxy
```

Use it with an existing OpenAI-compatible local backend:

```bash
forge-guardrails-proxy \
  --backend-url http://localhost:8080 \
  --port 8081
```

Then point OpenAI-compatible clients at `http://localhost:8081/v1`.
Requests should include their own `model` field. The proxy does not pick a
default upstream model unless you explicitly set `--model`, `FORGE_MODEL`, or
`SMALL_MODEL`; managed `ollama` still requires `--model`, and managed
`llamaserver` / `llamafile` use `--gguf`.

Add to your `Cargo.toml`:

```toml
[dependencies]
forge-guardrails = "0.1"
```

For development:

```bash
git clone https://github.com/whit3rabbit/forge-rs.git
cd forge-rs
cargo build
```

The Makefile wraps common development and eval commands. `make build` builds
all targets with the default `classifier` feature; override with
`FEATURES=""` for a no-feature build.

```bash
make build
make test
make clippy
```

The `forge/` submodule contains the Python reference for fixture generation and parity checks. Initialize it with:

```bash
git submodule update --init --recursive
```

### Release

Release is tag-driven. After the version in `Cargo.toml` is ready and `main`
is pushed, create and push a matching tag:

```bash
git tag v0.1.0
git push origin v0.1.0
```

The release workflow verifies the tag matches the crate version, runs format,
clippy, tests, `cargo package`, and `cargo publish`, builds platform archives
for `forge-guardrails-proxy`, publishes the GitHub release, then updates
`whit3rabbit/homebrew-tap` when `HOMEBREW_TAP_TOKEN` is configured. Users can
install the cask with:

```bash
brew install --cask whit3rabbit/tap/forge-guardrails-proxy
```

### Backend setup (pick one)

**llama-server** (recommended — top eval configs all run on llama-server):

Recommended model: [`mistralai_Ministral-3-8B-Instruct-2512-Q8_0.gguf`](https://huggingface.co/bartowski/mistralai_Ministral-3-8B-Instruct-2512-GGUF/blob/main/mistralai_Ministral-3-8B-Instruct-2512-Q8_0.gguf) (bartowski / HuggingFace)

```bash
# Install from https://github.com/ggml-org/llama.cpp/releases
llama-server -m path/to/mistralai_Ministral-3-8B-Instruct-2512-Q8_0.gguf --jinja -ngl 999 --port 8080
```

**Ollama** (alternative — easier setup):
```bash
# Install from https://ollama.com/download
ollama pull ministral-3:8b-instruct-2512-q4_K_M
```

**Anthropic** (API, no local GPU needed):
```bash
export ANTHROPIC_API_KEY=sk-...
```

See [Backend Setup](docs/BACKEND_SETUP.md) for full instructions.

## Quick Start

Run the proxy server as a reliability layer between your client agent and the LLM backend.

- **Run the proxy** on default port `8081` pointing to your local LLM backend:
  ```bash
  forge-guardrails-proxy --backend-url http://localhost:8080
  ```
- **Run on a different port** by specifying the `--port` flag:
  ```bash
  forge-guardrails-proxy --backend-url http://localhost:8080 --port 9090
  ```
- **Run with tool-output compression** using the `--tool-output-compression` flag to automatically compress prior tool results:
  ```bash
  forge-guardrails-proxy --backend-url http://localhost:8080 --tool-output-compression standard
  ```
- **Run with proxy input redaction** using the `--redact-secrets` flag to redact selected request text before it reaches the upstream model:
  ```bash
  forge-guardrails-proxy --backend-url http://localhost:8080 --redact-secrets
  ```
- **Run with classifier/validator** using the `--classify` flag to score model tool calls against the verifier ONNX model:
  ```bash
  forge-guardrails-proxy --backend-url http://localhost:8080 --classify
  ```

### Client & Backend Integration

Configure your agent clients or model backends to route through the proxy.

#### Claude Code Env Variables
Set the Anthropic base URL environment variable to point to the proxy:
```bash
export ANTHROPIC_BASE_URL="http://localhost:8081"
export ANTHROPIC_API_KEY="dummy"  # Or your actual key if proxying to Anthropic
```

#### Backends (llama-server / LM Studio / Ollama)
Point the proxy's `--backend-url` to your running model server:
- **llama-server** (default port 8080): `--backend-url http://localhost:8080`
- **LM Studio** (default port 1234): `--backend-url http://localhost:1234`
- **Ollama** (default port 11434): `--backend-url http://localhost:11434`

### Library Usage (Rust)

For direct library integration, use the `WorkflowRunner` in your Rust code:

```rust
use forge_guardrails::{
    Workflow, ToolDef, ToolSpec, ParamModel,
    WorkflowRunner, LlamafileClient,
    ContextManager, TieredCompact,
};
use std::collections::HashMap;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let client = LlamafileClient::new("path/to/model.gguf")
        .with_mode("native");

    let ctx = ContextManager::new(
        Box::new(TieredCompact::new(2)),
        8192,
    );

    let workflow = Workflow {
        name: "weather".into(),
        description: "Look up weather for a city.".into(),
        tools: HashMap::new(), // populate with ToolDef entries
        required_steps: vec![],
        terminal_tool: Some("get_weather".into()),
        system_prompt_template: "You are a helpful assistant. Use the available tools.".into(),
        prerequisites: vec![],
    };

    let runner = WorkflowRunner::new(client, ctx);
    runner.run(&workflow, "What's the weather in Paris?", None).await?;
    Ok(())
}
```

For multi-step workflows, multi-turn conversations, and backend auto-management, see [Eval Guide](docs/EVAL_GUIDE.md) and [Backend Setup](docs/BACKEND_SETUP.md).

## Proxy Server

Drop-in OpenAI-compatible (and Anthropic-compatible) proxy that sits between any client and a local model server. Point your client at the proxy and forge applies its guardrails transparently.

This is the path for **using forge with an existing harness** (opencode, Continue, aider, Cline, anything that speaks the OpenAI chat-completions schema). No rewrite.

```bash
# External mode — you manage the backend, forge proxies it.
cargo run --bin forge-guardrails-proxy -- \
  --backend-url http://localhost:8080 \
  --port 8081

# Managed mode — forge starts the backend and proxy together.
cargo run --bin forge-guardrails-proxy -- \
  --backend llamaserver \
  --gguf path/to/model.gguf \
  --port 8081

# Optional ONNX tool-call classifier shortcut.
cargo run --features classifier --bin forge-guardrails-proxy -- \
  --backend-url http://localhost:8080 \
  --classify \
  --port 8081

# Prefetch the quantized classifier artifact and print its location.
cargo run --features classifier --bin forge-guardrails-proxy -- --classify-download

# Convenience launcher for the recommended Ministral GGUF.
scripts/start_llamaserver_proxy.sh \
  /path/to/mistralai_Ministral-3-8B-Instruct-2512-Q8_0.gguf
```

The launcher uses managed `llamaserver` mode, verifies the GGUF path,
requires `llama-server` on `PATH`, checks that the proxy and backend ports are
free, and reuses an existing proxy binary from `PATH`, `CARGO_TARGET_DIR`, or
`target/`. If no binary is found, it falls back to `cargo build`.
Without an explicit path it searches for
`mistralai_Ministral-3-8B-Instruct-2512-Q8_0.gguf`; set `FORGE_MODELS_DIR` or
`MODELS_DIR` to point at your model directory. Defaults are proxy port `8081`
and managed backend port `8080`; override them with `FORGE_PROXY_PORT` and
`FORGE_BACKEND_PORT`. Press Ctrl+C to stop the proxy and its managed
`llama-server` backend.

The `--classify` shortcut is opt-in and requires building with
`--features classifier`. It downloads the pinned quantized tool-call ONNX
classifier if needed, stores it outside `target/`, enables advisory mode, and
prints the artifact directory during startup. By default it uses
`FORGE_CLASSIFIER_CACHE_DIR`, then `XDG_CACHE_HOME`, then
`$HOME/.cache/forge-guardrails/classifiers`. Use `--classifier-dir` to provide
an explicit artifact directory.

Secret input redaction is compiled in by default through the `secrets-scanner`
Cargo feature, but runtime redaction is off unless you pass
`--redact-secrets` or set `FORGE_REDACT_SECRETS=true`. When enabled, Forge
redacts proxy-bound input before upstream forwarding: OpenAI and Anthropic
message text, tool-result text, and prior assistant tool-call argument payloads.
It does not redact LLM responses, tool names, tool IDs, roles, model names, or
tool schemas. Builds made with `--no-default-features` reject
`--redact-secrets` and `FORGE_REDACT_SECRETS=true`.

Tool-output compression is enabled by default. It mutates only prior tool-result
content before forwarding a request upstream; tool calls, tool IDs, arguments,
and final responses are left unchanged. Start conservatively with `safe` or
`standard` (the default); dictionary compression requires explicit `aggressive` mode.

```bash
cargo run --bin forge-guardrails-proxy -- \
  --backend-url http://localhost:8080 \
  --tool-output-compression standard \
  --port 8081

cargo run --bin forge-guardrails-proxy -- \
  --backend-url http://localhost:8080 \
  --tool-output-compression aggressive \
  --tool-output-compression-method auto \
  --port 8081
```

See [Tool Output Compression](docs/COMPRESSION.md) for modes, request-level
`_forge` overrides, and method details.

Then configure OpenAI-compatible clients to use `http://localhost:8081/v1` as the API base URL. Anthropic-compatible clients should use `http://localhost:8081`; the proxy accepts Anthropic Messages API requests at `POST /v1/messages`. Requests without `model` are rejected unless you explicitly configured a fallback with `--model`, `FORGE_MODEL`, or `SMALL_MODEL`.

**Backend compatibility:**

- **Managed mode** spins up the backend for you. Supported backends: `llamaserver`, `llamafile`, `ollama` (use `--gguf` for GGUF-based backends, or `--model` for Ollama).
- **External mode** is backend-agnostic — forge talks `POST /v1/chat/completions` to whatever you point `--backend-url` at, as long as it speaks the OpenAI schema. Tool calls must come back in OpenAI `tool_calls` format or in one of forge's rescue-parsed formats (Mistral `[TOOL_CALLS]`, Qwen `<tool_call>` XML, fenced JSON).
- **Anthropic-compatible inbound** uses `anyllm_translate` for Anthropic/OpenAI conversion by default. With `--backend-protocol anthropic`, external mode sends Anthropic Messages requests to an Anthropic-shape downstream. Path 1 preserves block-level `cache_control` only on clean calls; retries, compaction, and context warnings rebuild the request and drop block metadata. Path 2 drops Anthropic-only block metadata at the OpenAI boundary.
- **Env-routed mode** remains a Rust extension for Docker/provider routing. If neither `--backend-url` nor `--backend` is passed, the binary uses existing anyllm/provider env vars such as `PROXY_CONFIG`, `OPENAI_BASE_URL`, and `BACKEND`.

This proxy does not enforce inbound authentication. Do not expose it publicly without a reverse proxy, network policy, or another auth layer.

### What proxy mode fortifies

On every `POST /v1/chat/completions`, forge applies (in order):

1. **Response validation** — each tool call in the model's response is checked against the `tools` array in the request. Calls to unknown tool names or with malformed shapes are caught before the response returns to your client.
2. **Rescue parsing** — when the model emits tool calls in the wrong format (JSON in a code fence, Mistral's `[TOOL_CALLS]name{args}`, Qwen's `<tool_call>...\</tool_call>` XML), forge extracts the structured call and re-emits it in the canonical OpenAI `tool_calls` schema.
3. **Retry loop with error tracking** — if validation fails, forge retries inference up to `--max-retries` (default 3) with a corrective tool-result message on the canonical channel, rather than returning a malformed response.
4. **Synthetic `respond` tool injection** — when tools are present in the request, forge injects a synthetic `respond` tool the model calls instead of producing bare text. The `respond` call is stripped from the outbound response — the client sees a normal text response (`finish_reason: "stop"`) and never knows the tool exists. Essential for small local models (~8B) that can't reliably choose between text and tool calls.

### What proxy mode does *not* do

Proxy mode is single-shot per request; some forge features need multi-turn workflow state that the OpenAI chat-completions schema doesn't carry:

- **Prerequisite enforcement and step-ordering** — these need a workflow definition spanning turns. Available in `WorkflowRunner`.
- **Context window management** — proxy mode does not choose or maintain the client's rolling message window. Default tool-output compression can rewrite prior tool-result content, and dedup can use an explicit `session_id`, but the client still owns conversation memory.
- **VRAM-aware budget detection** — opt in with `--budget-mode forge-full` or `--budget-mode forge-fast`; otherwise proxy uses the backend's reported budget. Env-routed mode can also use `FORGE_CONTEXT_TOKENS`.

### Useful flags

| Flag | Default | Purpose |
|---|---|---|
| `--backend-url URL` || External OpenAI-compatible backend |
| `--backend {llamaserver,llamafile,ollama}` || Managed backend type |
| `--model MODEL` || Model name, required for `ollama` |
| `--gguf PATH` || GGUF path, required for `llamaserver` / `llamafile` |
| `--backend-port N` | `8080` | Managed backend port |
| `--host HOST` | `127.0.0.1` | Proxy bind host in CLI mode |
| `--port N` | `8081` | Proxy bind port |
| `--max-retries N` | `3` | Retry budget per validation failure |
| `--classify` | off | Enable the quantized tool-call ONNX classifier in advisory mode |
| `--classify-download` | off | Download the quantized tool-call ONNX classifier and exit |
| `--redact-secrets` | off | Redact selected proxy-bound input before upstream forwarding |
| `--tool-output-compression {disabled,safe,standard,aggressive}` | `standard` | Compress prior tool-result content before upstream forwarding |
| `--tool-output-compression-method {lzw,repair,auto}` | `lzw` | Aggressive dictionary method |
| `--no-rescue` | rescue on | Disable rescue parsing |
| `--budget-mode {backend,manual,forge-full,forge-fast}` | `backend` | Context budget source |
| `--budget-tokens N` || Manual token budget |
| `--serialize` / `--no-serialize` | auto | Force request serialization |
| `--extra-flags -- FLAG VALUE ...` || Pass additional flags to the managed backend |

### Useful environment variables (Docker / env-routed mode)

| Variable | Default | Purpose |
|---|---|---|
| `FORGE_HOST` | `0.0.0.0` | Bind address |
| `FORGE_PORT` / `PORT` / `LISTEN_PORT` | `8081` | Forge proxy listen port |
| `FORGE_MODEL` / `SMALL_MODEL` | `(none)` | Optional fallback model when a request omits `model` |
| `FORGE_CONTEXT_TOKENS` | `128000` | Token budget |
| `FORGE_MAX_RETRIES` | `3` | Retry budget per validation failure |
| `FORGE_RESCUE_ENABLED` | `true` | Enable rescue parsing |
| `FORGE_SERIALIZE_REQUESTS` | `false` | Force request serialization |
| `FORGE_SENTRY_ENABLED` | `false` | Opt in to Sentry crash and aggregate guardrail telemetry |
| `FORGE_CLASSIFIER_CACHE_DIR` | platform cache | User-facing classifier download cache root |
| `FORGE_CLASSIFIER_DIR` || Local ONNX tool-call classifier artifact directory |
| `FORGE_CLASSIFIER_MODE` | `shadow` | `disabled`, `shadow`, `advisory`, or `enforce` |
| `FORGE_CLASSIFIER_MODEL` | `quantized` | `quantized` or `full` classifier ONNX file |
| `FORGE_REDACT_SECRETS` | `false` | Redact selected proxy-bound input before upstream forwarding |
| `FORGE_TOOL_OUTPUT_COMPRESSION` | `standard` | `disabled`, `safe`, `standard`, or `aggressive` |
| `FORGE_TOOL_OUTPUT_COMPRESSION_METHOD` | `lzw` | `lzw`, `repair`, or `auto`; used only by aggressive mode |
| `FORGE_START_SIDECAR` | Docker: auto | Start the internal anyllm sidecar in Docker |
| `ANYLLM_LISTEN_PORT` | Docker: `3000` | Internal anyllm sidecar port; do not publish it |
| `FORGE_SIDECAR_API_KEY` / `PROXY_API_KEYS` | generated | Shared Forge-to-sidecar key in Docker |
| `BACKEND` | `openai` | anyllm provider id or first-party backend |
| `OPENAI_BASE_URL` || Route to a local OpenAI-compatible backend |
| `OPENAI_API_KEY` || API key forwarded to the upstream |

Existing anyllm env and config are still honored, including provider API keys, `PROXY_CONFIG`, `BIG_MODEL`, `SMALL_MODEL`, and LiteLLM aliases such as `LITELLM_CONFIG`.

`FORGE_SENTRY_ENABLED=true` enables Sentry for the proxy binary only. Sentry
events are limited to scrubbed crashes and aggregate guardrail signals such as
classifier labels, retry exhaustion reasons, counts, and tool names. Prompts,
messages, headers, request bodies, tool arguments, tool outputs, and final
responses are not sent. Use `FORGE_TRAINING_CAPTURE_LOG` or
`FORGE_CLASSIFIER_LOG` for private local JSONL training/eval examples.

### Docker

You can run the Forge proxy as a Docker container. The image starts Forge plus an internal anyllm sidecar by default, and exposes only the Forge proxy port (`8081`) to clients. The sidecar is an upstream hop from Forge to anyllm; do not publish the sidecar port.

Build the image locally:

```bash
docker build -t forge-guardrails:local .
```

The default `Dockerfile` builds the normal proxy image without ONNX classifier
support. Use `Dockerfile.classifier` when you want the quantized tool-call
classifier artifact downloaded into the image and loaded on proxy startup:

```bash
docker build -f Dockerfile.classifier -t forge-guardrails:classifier .
```

The classifier image sets:

```text
FORGE_CLASSIFIER_DIR=/opt/forge/classifiers/tool-call/onnx
FORGE_CLASSIFIER_MODE=advisory
FORGE_CLASSIFIER_MODEL=quantized
```

Set `FORGE_CLASSIFIER_MODE=disabled` at runtime to use the classifier image as a
plain proxy. This image bundles only the ONNX classifier artifact; it does not
bundle a GGUF or provider LLM.

After publishing, replace `forge-guardrails:local` in these examples with `followthewhit3rabbit/forge-guardrails:latest`.

Run with OpenAI through the internal anyllm sidecar:

```bash
docker run --rm -p 8081:8081 \
  -e OPENAI_API_KEY=sk-... \
  -e FORGE_MODEL=gpt-4o-mini \
  forge-guardrails:local
```

Run the classifier-ready image the same way:

```bash
docker run --rm -p 8081:8081 \
  -e OPENAI_API_KEY=sk-... \
  -e FORGE_MODEL=gpt-4o-mini \
  forge-guardrails:classifier
```

The entrypoint generates a private Forge-to-sidecar key unless you set `FORGE_SIDECAR_API_KEY` or `PROXY_API_KEYS`. It starts the sidecar with the upstream provider environment, then starts Forge with `OPENAI_API_KEY` set to the sidecar key and `--backend-url http://127.0.0.1:3000`.

Start Ollama on the host in another terminal:

```bash
ollama pull qwen2.5-coder:14b
ollama serve
```

Then run the proxy container:

```bash
docker run --rm -p 8081:8081 \
  -e BACKEND=ollama \
  -e OPENAI_BASE_URL=http://host.docker.internal:11434/v1 \
  -e OPENAI_API_KEY=dummy \
  -e FORGE_MODEL=qwen2.5-coder:14b \
  forge-guardrails:local
```

Start llama-server on the host in another terminal:

```bash
llama-server \
  -m /path/to/model.gguf \
  --jinja \
  --host 0.0.0.0 \
  --port 8080
```

Then run the proxy container:

```bash
docker run --rm -p 8081:8081 \
  -e OPENAI_BASE_URL=http://host.docker.internal:8080/v1 \
  -e OPENAI_API_KEY=dummy \
  -e FORGE_MODEL=local-llama \
  forge-guardrails:local
```

On Linux Docker engines that do not define `host.docker.internal`, add:

```bash
--add-host=host.docker.internal:host-gateway
```

Smoke the running proxy:

```bash
curl http://localhost:8081/health

curl http://localhost:8081/v1/chat/completions \
  -H 'content-type: application/json' \
  -d '{"model":"qwen2.5-coder:14b","messages":[{"role":"user","content":"Say ok"}],"stream":false}'
```

OpenAI-compatible clients should use:

```text
base_url: http://localhost:8081/v1
api_key: dummy
model: qwen2.5-coder:14b
```

Claude Code can use the same Docker proxy through Forge's Anthropic-compatible endpoint:

```bash
unset ANTHROPIC_API_KEY
export ANTHROPIC_BASE_URL=http://127.0.0.1:8081
export ANTHROPIC_AUTH_TOKEN=dummy
export ANTHROPIC_MODEL=qwen2.5-coder:14b

claude --model qwen2.5-coder:14b
```

Do not add `/v1` to `ANTHROPIC_BASE_URL`; Claude Code sends Anthropic Messages requests and Forge serves those at `/v1/messages`. If you want Claude Code's model picker to query Forge's `/v1/models` endpoint, set `CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1`.

Publish to Docker Hub as `followthewhit3rabbit/forge-guardrails`:

```bash
docker login -u followthewhit3rabbit
scripts/publish_docker.sh
```

Override `VERSION`, `IMAGE`, `PLATFORMS`, or `BUILDER` when publishing a different tag or registry.

## Backends

| Backend | Best for | Native FC? |
|---------|----------|------------|
| **Ollama** | Easiest setup, model management built-in | Yes |
| **llama-server** | Best performance, full control | Yes (with `--jinja`) |
| **Llamafile** | Single binary, zero dependencies | No (prompt-injected) |
| **Anthropic** | Frontier baseline, hybrid workflows | Yes |
| **anyllm runtime** | In-process provider routing, OpenAI-compatible | Provider-dependent |
| **anyllm sidecar** | Separate process; admin UI, cache, metrics | Provider-dependent |

See [Backend Setup](docs/BACKEND_SETUP.md) for installation details.

## macOS / Apple Silicon

Apple Silicon is supported through all backends. Ollama can be installed with Homebrew or the official macOS download. llama.cpp / llama-server can be installed with Homebrew or a Metal-enabled release build. Llamafile works on macOS as a downloaded binary after `chmod +x`.

Managed llama.cpp and llamafile startup passes `-ngl 999`; on macOS that uses Metal rather than CUDA. Automatic Ollama context budgets use Rust VRAM tiers: <24 GB → 4096 tokens, 24–47 GB → 32768 tokens, ≥48 GB → 262144 tokens.

MLX is supported as an optional eval path on macOS through an OpenAI-compatible server such as `mlx_lm.server`, routed by `AnyLlmRuntimeClient` or `AnyLlmProxyClient`. It is not a managed `ServerManager` backend. Prefer llama-server for parity runs; treat GGUF-on-MLX as experimental.

```bash
uv tool install mlx-lm
mlx_lm.server --model mlx-community/Llama-3.2-3B-Instruct-4bit --port 8080
```

## Running Tests

```bash
cargo test
```

```bash
# Parity suite only (requires the Python golden fixture)
cargo test --test parity_tests

# With coverage (requires cargo-llvm-cov)
cargo llvm-cov --all-targets
```

Regenerate the Python golden fixture after intentional reference-behavior changes:

```bash
uv run --project forge python tests/parity/generate_fixtures.py
```

## Eval Harness

The eval harness measures how reliably a model + backend combo navigates multi-step tool-calling workflows. See [Eval Guide](docs/EVAL_GUIDE.md) for full CLI reference.

```bash
# 10-run release benchmark without classifier, with resource baseline enabled.
make eval-release

# 10-run release benchmark with classifier, with resource baseline enabled.
make eval-release-classify

# Fast smoke variants, also with resource baseline enabled.
make eval-smoke
make eval-smoke-classify

# Managed local smoke without the classifier
scripts/run_local_eval.sh --suite smoke --runs 1

# Managed local smoke with the user-cache classifier shortcut.
# Downloads or validates the quantized tool-call artifact before the proxy starts.
scripts/run_local_eval.sh --suite smoke --runs 1 \
  --classify \
  --classifier-mode shadow

# Python oracle against a running Rust proxy
python scripts/eval_openai_proxy.py \
  --base-url http://127.0.0.1:8081/v1 \
  --model test-model \
  --runs 10 \
  --stream \
  --scenario basic_2step sequential_3step error_recovery \
  --output eval_results_rust_proxy.jsonl

# Native Rust smoke runner
cargo run --bin forge-eval -- \
  --backend openai-proxy \
  --base-url http://127.0.0.1:8081/v1 \
  --model test-model \
  --runs 3 \
  --scenario basic_2step \
  --stream
```

Common Makefile overrides:

```bash
make eval-release OUTPUT_DIR=target/local-eval/release-baseline
make eval-release-classify CLASSIFIER_MODE=enforce
make eval-release RUNS=3 EVAL_ARGS="--skip-published-compare"
```

The Rust smoke runner supports `basic_2step`, `sequential_3step`, and `error_recovery` scenarios and emits JSONL for quick CI/smoke checks.

## Project Structure

```
src/
  lib.rs                     Public API re-exports
  error.rs                   ForgeError hierarchy
  server.rs                  setup_backend(), ServerManager, BudgetMode
  classifier_download.rs     Classifier artifact download logic (--features classifier)
  tool_output.rs             Tool-output compression pipeline (safe / standard / aggressive)
  tool_policy.rs             Per-request allowed/blocked tool sets and prerequisite policy
  core/
    message.rs               Message, MessageRole, MessageType, MessageMeta, ToolCallInfo
    tool_spec.rs             ToolSpec, ToolDef, ParamModel — tool schema and callable defs
    workflow.rs              Workflow model, terminal tools, prerequisites
    steps.rs                 StepTracker, step tracking and required-step state
    inference.rs             run_inference() — shared front half (compact, fold, validate, retry)
    runner.rs                WorkflowRunner — the agentic loop
    slot_worker.rs           SlotWorker — priority-queued slot access
  guardrails/
    guardrails.rs            Guardrails facade — applies the full stack in foreign loops
    nudge.rs                 Nudge dataclass
    response_validator.rs    ResponseValidator, ValidationResult
    step_enforcer.rs         StepEnforcer, StepCheck, StepPrerequisite
    error_tracker.rs         ErrorTracker
    scoring.rs               ScoringPipeline, ScoringExecutor — async classifier dispatch
    scoring_context.rs       ScoringContext — serialized input for ONNX scorer
    classifier_artifact.rs   Artifact loader, manifest validation, threshold policy
    onnx_scorer.rs           OnnxToolCallScorer, OnnxFinalResponseScorer (--features classifier)
    history.rs               Events timeline for validation results and violations
    policy.rs                Allowed/blocked tool policy based on sequence prerequisites
  clients/
    base.rs                  LLMClient trait, ChunkType, StreamChunk, LLMCallInfo, TokenUsage
    sampling.rs              Model sampling defaults, MODEL_SAMPLING_DEFAULTS
    anthropic/               AnthropicClient (frontier baseline, native FC)
    llamafile/               LlamafileClient (native FC or prompt-injected)
    ollama/                  OllamaClient (native FC)
    anyllm_proxy.rs          AnyLlmRuntimeClient, AnyLlmProxyClient
  context/
    manager.rs               ContextManager, CompactEvent
    strategies.rs            NoCompact, TieredCompact, SlidingWindowCompact
    hardware.rs              HardwareProfile, detect_hardware()
  prompts/
    mod.rs                   Tool prompt builders (prompt-injected path)
    nudges.rs                Retry, step-enforcement, and semantic classifier nudge templates
    parse_strategies.rs      Rescue parsing: Mistral, Qwen, fenced JSON
  tools/
    respond.rs               Synthetic respond tool (respond_tool(), respond_spec())
  proxy/
    handler.rs               Request handler — bridge between HTTP and run_inference
    proxy.rs                 OpenAI messages ↔ forge Messages conversion, SSE helpers
    server.rs                HTTPServer — axum HTTP/SSE server
  bin/
    forge-guardrails-proxy.rs  CLI proxy entry point
    download-classifier.rs     Standalone artifact downloader for eval / training paths
    forge-eval/                Native Rust eval smoke runner
model/
  README.md                  Artifact repository index and download commands
  MODEL.md                   Full model card: training config, metrics, labels, thresholds
tests/
  parity/                    Python-generated golden fixtures for Rust parity tests
  parity_tests.rs            Rust assertions against python_golden.json
  engine_tests.rs            WorkflowRunner / inference integration tests
  guardrails_tests.rs        Guardrails and step enforcement tests
  compact_tests.rs           Compaction strategy tests
  context_tests.rs           ContextManager tests
  *_tests.rs                 Unit and integration tests per subsystem
scripts/
  eval_openai_proxy.py       Python eval oracle wrapper for Rust proxy checks
docs/
  CLEANROOM.md               Clean-room run summary and parity review
  PARITY.md                  Parity contract and subsystem alignment status
  EVAL_GUIDE.md              Eval harness CLI reference
  BACKEND_SETUP.md           Backend installation and server setup
  COMPRESSION.md             Tool-output compression modes and request overrides
```

## Public API Surface

The crate re-exports the main building blocks from `src/lib.rs`:

```rust
use forge_guardrails::{
    // Backends
    AnthropicClient, LlamafileClient, OllamaClient,
    AnyLlmRuntimeClient, AnyLlmProxyClient,
    // Client trait and types
    LLMClient, LLMResponse, StreamChunk, TextResponse, ToolCall, TokenUsage,
    // Workflow
    WorkflowRunner, Workflow, ToolDef, ToolSpec, ParamModel,
    // Context
    ContextManager, NoCompact, SlidingWindowCompact, TieredCompact,
    // Guardrails
    Guardrails, StepEnforcer, ErrorTracker, ResponseValidator,
    // Scoring pipeline (async classifier dispatch)
    ScoringPipeline, ScoringExecutor, ScoringContext,
    // Step tracking
    StepTracker, SlotWorker,
    // Prompts and nudges (including semantic classifier nudges)
    retry_nudge, step_nudge, prerequisite_nudge, unknown_tool_nudge,
    classifier_nudge, rescue_tool_call, build_tool_prompt,
    // Proxy / server
    handle_chat_completions, handle_anthropic_messages,
    HTTPServer, ServerManager, setup_backend,
};
```

## Usage Modes

### 1. Workflow runner

Use `WorkflowRunner` when you want the library to manage the LLM loop: system prompt construction, message folding, validation, retries, tool execution, context compaction, and terminal-tool detection.

### 2. Guardrails middleware

Use the guardrail primitives directly when you already own the orchestration loop but want validation and policy enforcement.

Relevant pieces:

- `Guardrails` — composable facade for the full stack
- `ResponseValidator` / `ValidationResult`
- `StepEnforcer` / `StepCheck` / `StepPrerequisite`
- `ErrorTracker`
- `retry_nudge`, `step_nudge`, `prerequisite_nudge`, `unknown_tool_nudge`, `classifier_nudge`
- `ScoringPipeline` / `ScoringExecutor` — async classifier dispatch for shadow, advisory, and enforce modes
- `ScoringContext` — serializes workflow state into the canonical `toolcall-verifier-input/v1` format for the ONNX scorer

The ONNX tool-call verifier and final-response verifier are built with `--features classifier`. Both start in `shadow` mode and are promoted only after eval replay proves safety. See [model/README.md](model/README.md) for artifact contracts, labels, thresholds, and promotion criteria.

### 3. OpenAI-compatible proxy / server layer

Use the proxy and HTTP server pieces when you need an OpenAI-compatible request/response boundary around a backend.

Relevant pieces:

- `openai_to_messages`, `tool_calls_to_openai`, `text_response_to_openai`
- `text_to_sse_events`, `tool_calls_to_sse_events`
- `handle_chat_completions`, `handle_anthropic_messages`
- `HTTPServer`, `ServerManager`, `setup_backend`

### 4. anyllm runtime and sidecar integration

Use `AnyLlmRuntimeClient` for in-process anyllm provider routing (no HTTP overhead; Forge still owns interception and nudging):

```rust
use forge_guardrails::AnyLlmRuntimeClient;

let client = AnyLlmRuntimeClient::from_multi_config(
    "gpt-4o-mini",
    anyllm_proxy::config::MultiConfig::load().multi_config,
)
.with_context_length(128_000);
```

Use `AnyLlmProxyClient` when you prefer to run `anyllm_proxy` as a separate sidecar process. The sidecar URL is an upstream hop from Forge to anyllm, not the public client-facing Forge proxy URL. Keep the sidecar private and expose only the Forge proxy unless you intentionally need direct anyllm access.

```rust
use forge_guardrails::AnyLlmProxyClient;

let client = AnyLlmProxyClient::new("gpt-4o-mini")
    .with_base_url("http://127.0.0.1:3000")
    .with_api_key("local-proxy-key")
    .with_context_length(128_000);
```

Both clients expose provider observability through `LLMClient::last_call_info()`. Cost estimates, routing metadata, cache state, and rate-limit details come from anyllm runtime or sidecar metadata; Forge does not maintain separate pricing logic.

## Testing Scope

- 487+ passing tests across 16 test files
- Deterministic parity suite against `tests/parity/fixtures/python_golden.json`
- Classifier tests (`--test classifier_tests`) cover artifact loading, serializer parity, ONNX scorer output, and scoring pipeline routing
- 0 contamination incidents in the clean-room run

Keep tests deterministic where possible. Backend integration tests use mock servers (via `mockito`) unless they intentionally qualify a live backend. Classifier tests require `--features classifier` and the pinned ONNX artifact; they are gated separately from the core test suite.

## Known Review Areas Before Release

The implementation should be reviewed for protocol correctness and production hardening before publication or deployment. Behavioral parity with the Python reference is covered by the parity test suite; the following areas need additional protocol and integration review:

- tool-call ID pairing across assistant tool calls and tool results
- transcript validity after guardrail-blocked steps
- compaction behavior around tool-call / tool-result groups
- true progressive streaming behavior for each backend
- HTTP parsing and CORS/header handling if exposed beyond local development
- backend startup ordering and context-budget discovery
- serialization behavior for OpenAI, Ollama, and Anthropic formats

## Python Parity

The parity suite compares Rust behavior to synthetic golden outputs generated by the Python reference submodule. The source of truth for fixture generation is `tests/parity/generate_fixtures.py`; the checked-in output is `tests/parity/fixtures/python_golden.json`; Rust assertions live in `tests/parity_tests.rs`.

When updating parity behavior:
1. Add or update the Python fixture first.
2. Regenerate `tests/parity/fixtures/python_golden.json`.
3. Add or update the matching Rust assertion in `tests/parity_tests.rs`.
4. Run `cargo test --test parity_tests` before broader repo gates.

See [docs/PARITY.md](docs/PARITY.md) for the full parity contract.

## Relationship to Upstream Forge

The upstream Forge project is a Python reliability layer for self-hosted LLM tool-calling and multi-step agentic workflows. This repository is a Rust implementation inspired by that project's behavior — not a direct source translation — and has been verified for full behavioral parity with the Python reference through the parity test suite.

The Python reference is included as the `forge/` git submodule for use in fixture generation and parity checks.

Use the upstream repository for the original Python implementation, documentation, paper citation, and release history:

- <https://github.com/antoinezambelli/forge>

The forge guardrail framework and ablation study are published as:

> Zambelli, A. *Forge: A Reliability Layer for Self-Hosted LLM Tool-Calling.*
> [https://doi.org/10.1145/3786335.3813193]https://doi.org/10.1145/3786335.3813193

## License

[MIT](LICENSE) — Rust implementation copyright (c) 2025-2026 whit3rabbit.

The upstream Forge project is separately licensed by its author as MIT as well. Preserve upstream attribution and review license compatibility before redistribution.