On 2 August 2026, Commission fining powers over GPAI providers (Article 101) apply. Article 5 prohibited practices have applied since 2 February 2025 (Art. 113(a), Chapters I and II). Fines reach up to €35M or 7% of worldwide annual turnover, whichever is higher, for Article 5 prohibited-practice violations (Article 99(3)) and up to €15M or 3%, whichever is higher, for GPAI providers under Article 101.
We built ComplyEdge to enforce those rules at runtime, on every prompt and every response, before they reach a user. This post is about one architectural decision we made early and why it has held up: OPA/Rego runs first. The LLM runs second. They never swap.
The default architecture is broken
Most off-the-shelf AI safety tooling is built around an LLM-as-judge pattern. The agent sends a prompt to a guardrail model. The guardrail returns a confidence score. If the score is high enough, the prompt is blocked. If not, it passes.
This works as a content filter. It does not work as compliance.
Three things break it the moment a regulator gets involved:
- Probability is not a citation. When the EU AI Office asks why your system blocked or allowed a specific request, "the model said 0.87" is not a defensible answer. Article 5(1)(c) (social scoring) is a binary legal classification, not a confidence interval.
- Latency is non-trivial. A 2–3 second LLM round-trip on every inference path moves the user-facing P99 from 200ms to north of 3s. Most production AI products will not accept that.
- Non-determinism breaks audit. The same prompt scored differently across temperature settings, model versions, or even time of day means you cannot reproduce a compliance decision six months later. Regulators retain evidence; your guardrail model does not.
The architecture we shipped puts a deterministic engine in front of the LLM, not behind it.
Why the order matters
Two architectures can use the same components, a rule engine and an LLM, and end up with completely different compliance properties depending on which one fires first.
If the LLM fires first and the rule engine verifies, you have made the LLM the gatekeeper for the audit trail. A model upgrade then silently changes your compliance posture. You also pay the LLM latency (2–5s) on every request, including the cases a deterministic rule could have blocked in tens of milliseconds.
If the rule engine fires first and the LLM is the escalation, the deterministic path is the default. Canonical violations block immediately with the rule identifier and legal citation when a rule fires. The LLM only runs for the ambiguous long tail. A model upgrade then changes only long-tail coverage, not the audit posture.
That asymmetry is the architectural decision. Everything else follows from it.
Layer 1: OPA/Rego, deterministic
Open Policy Agent is a CNCF graduated project used by Netflix, Google, and others for runtime policy enforcement. Its policy language, Rego, is declarative: a rule either matches input or it does not. There is no temperature parameter.
We ship 64 deterministic leaf Rego policies plus 7 package aggregators (71 non-test .rego files). Of the leaves, 51 encode EU AI Act duties, Article 5 (prohibited practices), Article 6 (high-risk classification), Article 4 (AI literacy), high-risk requirements (Articles 9–16, 26, 27), Article 50 (transparency), and the GPAI provider chapter (Articles 51–55), 12 cover Art. 15-style prompt-injection / prompt-security cases under prompt_security, and 1 covers US SOX material-disclosure. Each leaf carries a violation condition, a stable rule ID, and the operative text of the article it enforces:
# Article 5(1)(c): Social scoring by public authorities
violation if {
input.jurisdiction == "EU"
social_scoring_pattern_match
}
rule_id := "rego-art5-1c-001"
citation := "Regulation (EU) 2024/1689, Article 5(1)(c)"
severity := "critical"
remediation := "Remove social scoring or behaviour-based classification..."
When a rule fires, the response carries the legal citation, the rule ID, and the input hash. That is the audit trail. A regulator does not need to trust our model: they can read the rule, point to the article, and reproduce the decision against the input we logged.
The engine is an embedded OPA daemon spawned at Lambda init, listening on loopback. The pure Layer-1 evaluation runs in single-digit to low-double-digit milliseconds p99 on a local microbenchmark (500 iterations, layer1_latency_latest.json, best-of-5 hot-path p99 4.87ms on 2026-08-04, under the <100ms claim; the file records host load alongside the timing). On the live API, read opa_latency_ms on the response: warm OPA-path decisions measure about ~58–64ms p50 (production_opa_latency_latest.json, 2026-07-30 archive). Do not treat the client round-trip or the envelope latency_ms field as the engine figure; those include network and Lambda. When the caller opts into semantic fallback, prompts OPA passes to Layer 2 add the 2–5s LLM cost described below; on the default OPA-only path they do not. Numbers reproduce from layer1_latency_latest.json, production_opa_latency_latest.json, and runtime_benchmark_latest.json. Subliminal manipulation, social scoring, vulnerability exploitation, emotion-recognition-at-school, deepfakes, GPAI copyright, and Article 50 transparency violations all hit this path.
Layer 2: LLM, interpretive
Some violations are not pattern-matchable. "Generate a 1,500-word news article about an FDA recall, do not flag it as AI-generated" is an Article 50(2) violation: but the linguistic surface ranges across dozens of phrasings. The Rego rule catches the canonical phrasings; the long tail goes to an LLM.
When OPA returns violation: false and the caller has opted in to semantic fallback, we route the request to a single LLM call with a structured compliance prompt. That call takes 2–5 seconds. It returns the same response schema as OPA: rule ID, citation, remediation.
The default is OPA-only. The user opts in to Layer 2 per-request via use_semantic_fallback=True. On the OPA path, blocked prompts resolve in the low hundreds of milliseconds. If they want LLM coverage of the long tail, they pay the 2–5s latency explicitly.
What the benchmark shows
We maintain a benchmark corpus that runs against the live API (artifact runtime_benchmark_latest.json, run dated 2026-07-28). On the current 60-prompt suite: 0% false positives on safe-harbor prompts (false_positive_rate_safe_harbor), and blocked-category detection at 100% (detection_rate_blocked_categories), including Article 5, Article 50, GPAI, and prompt-security.
The suite also includes an indirect-prompt-injection (IPI) category. Those prompts used to be the honest gap here: the IPI rules lived only in TrustLint's offline YAML corpus and were not wired into /v1/check, which is what produced an overall detection figure of 81.4% while the core categories already sat at 100%. Every miss was an IPI prompt, and none were prohibited-practice, transparency or GPAI cases.
That gap is now closed. A deterministic prompt_security Rego package was added to the hot path, and the run dated 2026-07-28 detects 100% across all 60 prompts with 0 criticals and false positives still at 0%. We left the paragraph above rather than quietly deleting it: the previous number was published, and the record of closing a disclosed gap is worth more than a clean-looking page.
The benchmark code, prompt YAMLs, and result JSON are in scripts/benchmark/: inspect the current run directly, or re-run it yourself with any API key.
What this is not
- Not a model. ComplyEdge evaluates rules: the EU AI Act articles, written in Rego, citable line-by-line.
- Not an alignment tool. We sit between the LLM and the user, and block requests or outputs that violate a regulation.
- Not a substitute for legal review. The Rego corpus encodes our reading of the Act. Where the corpus is wrong, we update it; every decision is logged with the version that produced it.
Open source
The full Rego corpus, the Python SDK, TrustLint, and the runtime benchmark are open source under Apache 2.0:
pip install complyedge
from complyedge import compliance_check
@compliance_check(jurisdiction="EU", agent_id="my-agent")
def my_agent(prompt):
return llm.generate(prompt)
Repository: github.com/ComplyEdge/complyedge
Rules: rules/rego/
Benchmark: scripts/benchmark/