12 min read

Introducing Foxfence: make any model reliable — and safe — behind your agent

An open-source, single-binary proxy that sits between your agent and any model, and makes a cheap model both more capable and safer. From Zenfox AI.

Introducing Foxfence: make any model reliable — and safe — behind your agent

An open-source release from Zenfox AI. Apache 2.0. One binary, one config file, zero telemetry.

You picked a model for your agent. Maybe it's a 7B you self-host to keep costs down, maybe it's an open-weight frontier model on a serverless endpoint. Then you wire up tools — and things get wobbly.

The model emits malformed JSON arguments. It wraps a tool call in a markdown fence. It hallucinates a field that isn't in your schema. Or — and this one surprises people — you get no usable tool calls at all: the endpoint or provider you routed through doesn't expose them, the model's format doesn't line up with what your client expects, and your agent just gets prose where it expected a function call.

At the same time, the agent layer is wide open. The same model will happily echo a secret you pasted into a prompt, or call whatever tool it decides to, rm -rf included. You wanted "cheap and flexible." You got "unreliable and unguarded."

The usual fixes are heavy. An enterprise guardrails gateway brings a database, a web console, and a deployment story. A cost-routing layer gets you failover and price arbitrage but doesn't care whether a tool call is valid or whether a credential just leaked. We wanted something smaller and sharper — a single thing that sits on the wire and makes the model behind it both more capable and safer, without becoming a platform.

That's foxfence.

What it is

foxfence is a stateless, drop-in proxy that speaks the OpenAI API on both sides. Your agent — OpenClaw, Aider, Cline, the official SDKs, anything OpenAI-compatible — points at foxfence. foxfence points at your model: Ollama, vLLM, OpenRouter, Together, llama.cpp, a proprietary API. Nothing in your agent changes; you swap a base URL and a model name.

It does three jobs on one connection:

  1. A capability shim — makes tool calling and structured output reliable, even for models that don't support them natively (or support them badly).
  2. A reliability layer — stateless transforms that catch the ways small / self-hosted models fall apart over a multi-turn session: re-firing a failed tool call forever (loop-breaker), drifting off the system prompt across long tool-heavy chats (re-grounding), choking on a strict chat template (template hygiene), and hanging on a stalled response (upstream timeout).
  3. A safety layer — inline detectors on requests and responses: secret/PII masking, prompt-injection detection via external classifiers, and a tool-call policy engine that runs on the parsed tool calls.

The one-sentence pitch:

Plug any cheap model into your agent: it becomes more capable, more reliable, and safer.

And, just as importantly, what foxfence is not: it's not an agentic loop (one request in, one enriched turn out), it's not a multi-tenant enterprise gateway (no DB, no admin UI, no user management), and it's not a cost router. One YAML file is the entire behavior surface. The config is the documentation.

The proof: it's been measured against real models

Most "make your model better" claims come with a hand-wave. foxfence ships a reproducible eval harness (bun run eval), and we point it where the proxy actually earns its keep: small, open, self-hostable models. The headline metric is the valid tool-call rate — a schema-valid call to the right tool (or correctly not calling one). direct is the model called straight; foxfence is the same model behind the proxy on shim: auto.

Scenariodirectfoxfencewhat happened
Qwen 2.5 7B — tool calling83%90%runtime native→prompted downgrade + repair loop recovered malformed calls
Llama 3.1 8B — tool calling86%100%also fixed a spurious direct tool call
Qwen 2.5 7B — stuck in a retry loop67%100%the loop-breaker nudged it out of re-firing the same failed call
a model with no native tools (bundled simulator)0%~86%the json-prompted shim + repair loop

Read it top to bottom and you get the value proposition:

  • It improves small native models. Qwen and Llama gained points from the runtime strategy-downgrade and the repair loop firing on genuinely malformed output.
  • It rescues models that can't tool-call at all. The bundled simulator shows the deterministic 0% → ~86% rescue — the prompted protocol plus schema validation does the work.
  • It breaks the loops small models get stuck in. Handed a history of the same failed call fired three times, Qwen kept re-firing it; the loop-breaker carried it to a clean recovery — and a break mode stops the loop deterministically when you want a hard cap.
  • It stays transparent on models that already work. We dropped the frontier-model rows (GPT-4o, Kimi, GLM, gpt-oss…) from the eval on purpose: they sat at native parity — transparent, zero repairs, the same with and without foxfence — so the number measured the model, not the proxy. On those, foxfence's value is the reliability and safety layers, not a capability boost.
  • The benchmark keeps us honest. Every number is yours to reproduce — the harness ships in the repo; run the eval on your endpoint and you get your numbers.

We even forced the prompted shim on a model that does have native tools: it beat Qwen's own native path (76% → 100%). A prompted protocol plus strict schema validation can be more reliable than a model's built-in tool calling.

A short tour

The tool-calling shim

When your agent sends tools, foxfence figures out the model's effective capability (a tiny one-time probe, memoized in memory) and picks a strategy:

  • native — the model handles tools well; foxfence passes through untouched but still validates every call against its JSON Schema before it reaches the agent.
  • json-prompted — describes the tools in an injected system block and parses the reply tolerantly (markdown fences, prose around the object, arguments-as-string).
  • constrained — when the upstream supports constrained decoding (vLLM guided_json, OpenAI/llama.cpp json_schema), foxfence sends a per-tool union schema so the server can't emit a malformed call.
  • react — a Thought / Action / Action Input format for tiny or old models that can't hold JSON.

Whatever the strategy, three things hold: tool arguments are validated against their schema, a bounded repair loop sends malformed output back to the model with the precise error (capped, accounted for, and visible in a response header — never a hidden agent loop), and a model that was classified native but starts slipping is automatically downgraded at runtime and the event is logged. It's the difference between "tool calling works in the demo" and "tool calling works in production."

Reliability for the ways small models actually fail

Getting one tool call right is only half the battle with a cheap model — the other half is what happens over a whole session. foxfence adds four stateless transforms, each computed from the request alone (no hidden state) and additive, so on a model that doesn't need them it stays out of the way:

  • Loop-breaker. When a tool call fails, small models love to re-fire the identical call and spin your agent forever. foxfence spots the repeat in the request history and either nudges the model to change approach (default) or stops the loop outright (break). On Qwen 2.5 7B this took loop-recovery from 67% to 100%.
  • Re-grounding. Over a long, tool-heavy conversation a small model drifts off its system prompt and forgets the rules it was handed. Once enough tool results pile up, foxfence re-asserts the original system prompt near the end of the request, so the constraint is back in the model's attention.
  • Template hygiene. Many small models break when their chat template is violated — no system role, no tool role, strict role alternation. Declare the quirk on the model's profile and foxfence reshapes the request to fit: fold the system message into the first user turn, rewrite tool messages, merge consecutive turns.
  • Fail-fast upstream timeout. A model that hangs — say a reasoning model emitting an unbounded thinking trace — returns a clean error instead of blocking your agent indefinitely.

These are the failure modes that don't show up in a one-shot demo and absolutely show up in production on a 7B.

The safety layer

  • Secrets — mask & restore. High-precision detection (cloud keys, tokens, PEM blocks, connection strings, JWTs, with an entropy gate for generic sk-… keys). A secret in a prompt is replaced with a placeholder before it reaches the model and restored in the reply; a brand-new secret the model invents is redacted for good.
  • Tool-call policy — the differentiator. A declarative allow/deny policy enforced on the parsed tool calls, after the shim has decoded them — so it works even for models with no native tool calling, something a text-level firewall can't do. Glob or regex matching on tool name and arguments, default: deny allowlisting, and a clean compile-time check that rejects a catastrophic regex before it can ever run. A blocked call comes back as in-band feedback the agent can recover from, not an HTTP error.
  • Remote detectors. Point any detector at an external classifier (LLM Guard, OpenGuardrails, your own model behind a small HTTP server) and foxfence POSTs content to it — this is how prompt-injection scanning plugs in. The core stays tiny; heavy detection is opt-in and external.

We're honest about the boundary, too: argument-content matching is defense-in-depth, not a sandbox. The docs say so plainly, and the recommendation is allowlisting for anything that truly matters.

Streaming that doesn't cheat on safety

Streaming is where guardrails usually quietly give up. foxfence streams native and tool-free responses token-by-token, and the hard part — never emitting a prefix of a secret before the whole secret has been seen and redacted, no matter how the secret is split across SSE chunks — is the most heavily tested code in the project (a property test that exercises every chunk boundary). Tool calls are assembled, validated, and run through the policy engine before they're emitted as clean chunks. The client never sees the intermediate format.

Profiles, metrics, and a second API surface

  • Model profiles are a community cache of observations — pin a model's capabilities for deterministic production, or record a chat-template quirk (Gemma's lack of a system role, say) the probe can't see.
  • Prometheus /metrics (opt-in) exposes added latency, repair rate per model, and detector verdicts.
  • Both /v1/chat/completions and /v1/responses are supported, the latter translated to the same internal pivot so every shim and safety feature applies to both (the Responses surface is non-streaming for now — stream via /v1/chat/completions).

Why it's built the way it is

foxfence is a security component, so its own attack surface is its first quality criterion. The design reflects that:

  • Stateless. No DB, no sessions; all context comes from the request. Horizontally scalable, hot-restartable, trivially auditable.
  • Tiny, auditable core with a small set of pinned dependencies and bun audit in CI. Heavy detection lives outside the binary.
  • Transparent by default. If no rule matches, the request passes through byte-for-byte. foxfence must never degrade a model that already works — and the eval proves it doesn't.
  • Configurable fail-closed. When a detector errors, the behavior (block or pass) is explicit in your config, never implicit.
  • Zero telemetry. The only network calls foxfence makes are to the upstreams and classifier URLs you declared. Secrets come from the environment and are never written to the audit log.
  • Signed releases. Multi-platform binaries, Sigstore keyless signatures, published checksums.

Where it fits

If you need multi-tenant billing, an admin console, and load balancing, reach for an enterprise gateway. If you need cost routing across providers, reach for a router. foxfence deliberately does neither. It does one thing: it sits between your agent and your model and makes that pairing reliable and safe — as a single binary you can read end to end in an afternoon.

Get started

Grab a signed binary from the releases page, or build it from source — the whole project lives at github.com/zenfoxai/foxfence:

bun install && bun run build      # → dist/foxfence

# point it at your model
cp foxfence.example.yaml foxfence.yaml
./dist/foxfence --config foxfence.yaml

Then point your agent at http://localhost:4100/v1 with the exposed model name. That's it — tool calling gets reliable, secrets get masked, and your tool policy is enforced, with no changes to the agent. Want to see the numbers on your own model?

bun run eval --endpoint <your-endpoint> --model <your-model> --key <your-key>

Open source, from Zenfox AI

foxfence is open source under Apache 2.0, built and maintained by Zenfox AI — the code is at github.com/zenfoxai/foxfence. It's the kind of infrastructure we wanted and couldn't find: small enough to trust, sharp enough to matter, and honest enough to publish the benchmark next to the claim.

Try it, read the source, run the eval against your own stack — and if you verify how a model behaves on your deployment, contribute a profile. The most useful thing you can send us is something you measured.

github.com/zenfoxai/foxfence

— The Zenfox AI team · github.com/zenfoxai/foxfence · zenfox.ai