Skip to content

Backends ​

A backend is how DecGuard reaches a decision model. Every backend implements the same small interface and returns the same validated DecisionResult, so one contract and one dataset run unchanged against any of them.

Choosing a backend ​

ProviderUse it forPage
mockoffline, deterministic tests, demos and CI; simulating defectsbelow
systemoneSystem One decision APIs: self-hosted Kev, Jev through OpenRouter, compatible TypeSafe endpointsSystem One
httpany endpoint that speaks DecGuard's small JSON protocol, or a thin proxy in front of your modelGeneric HTTP
plugina Python package registering a provider under decguard.backendsCustom backends
CallableBackenda Python function, from the Python API onlyCustom backends

Default and named backends ​

The contract's backend is the default. backends declares named alternatives: the model you are replacing, a candidate, a staging endpoint, a fallback target for a policy.

yaml
backend:                       # "default"
  provider: mock
  model: refund-mock-v1
  default: {refund: 0.2, reject: 0.15, review: 0.65}

backends:
  kev:
    provider: systemone
    model: kev-latest
    url: http://127.0.0.1:8009/v1/systemone
  jev:
    provider: systemone
    model: typesafe/jev-1.13
    url: https://openrouter.ai/api/alpha/decisions
    bearer_token_env: OPENROUTER_API_KEY
bash
decguard test decguard.yaml                 # default
decguard test decguard.yaml --backend jev   # named

Every backend has provider (required) and model (optional for most providers, required for systemone). All other keys are provider settings, validated by the provider: unknown keys are errors. decguard validate builds every declared backend without making network calls, so configuration mistakes surface before a run. The name default is reserved for the top-level backend.

Credentials ​

Credentials come only from environment variables. A contract names the variable, never the value:

yaml
backend:
  provider: systemone
  model: typesafe/jev-1.13
  url: https://openrouter.ai/api/alpha/decisions
  bearer_token_env: OPENROUTER_API_KEY   # sends Authorization: Bearer $OPENROUTER_API_KEY

The HTTP-based backends (http, systemone) enforce this:

  • bearer_token_env sends Authorization: Bearer <value>; headers_from_env maps any other header to a variable.
  • Literal Authorization or API-key headers, credentials embedded in the URL, and credential-like query parameters (api_key, token, key, ...) are rejected.
  • Variables are read when the first request is made. A missing variable stops the run with exit code 2.
  • With environment-backed headers, health_url must share the backend URL's origin, so a healthcheck cannot forward a credential elsewhere.
  • Reports store URLs without their query string, redirects are not followed, untrusted HTTP error bodies are not recorded, and values under common credential keys in backend metadata are redacted.

In CI, map the variable from a secret; see CI.

Healthchecks ​

Before a run, test, fuzz and run call the backend's healthcheck (disable with --no-healthcheck). The result is ok, unhealthy or unknown; only unhealthy stops the run (exit code 2). HTTP backends are unknown unless health_url is set, in which case any 2xx answer to a GET is healthy. The result is stored in the report's health.

Validation and errors ​

Adapters only return raw label probabilities. DecGuard measures latency and validates the answer: exactly the contract's labels, finite, non-negative, summing to 1 within evaluation.probability_tolerance. Anything else is an invalid_response error for that case — counted and reported, never repaired. Per-case failures are typed (timeout, unavailable, invalid_response, backend_error, exception); see Errors are results too.

If every case fails, or the healthcheck reports unhealthy, the run stops with exit code 2.

Backends are called from up to evaluation.max_concurrency threads at once (default 4).

mock ​

A deterministic, offline backend for tests, examples and CI. It needs no network and always gives the same answer for the same input.

yaml
backend:
  provider: mock
  model: refund-mock-v1
  rules:                       # first match wins, ignoring case and whitespace
    - contains: "damaged"
      probabilities: {refund: 0.96, reject: 0.01, review: 0.03}
  default: {refund: 0.2, reject: 0.15, review: 0.65}   # when no rule matches
  seed: 0                      # without `default`: a seeded hash of the input decides
  model_version: "1"
  position_bias: 0.0           # deliberate defect: mass moved to the option shown first
SettingDefaultMeaning
rules[]checked in order; a rule matches when contains appears in the input (case- and whitespace-insensitive; object inputs are matched on their JSON form)
defaultnonedistribution when no rule matches
seed0without default, unmatched inputs get a pseudo-random distribution from a seeded hash of the input, stable across processes
model_versionnonereported as the result's model_version
position_bias0.0share of probability mass (0–1) moved to the option presented first

Rule and default distributions are validated against the contract labels up front. The mock reads options the way a well-behaved model would: reordered options and reformatted labels (REFUND, B) refund, "refund", [refund]) get the same answer, so it passes the metamorphic properties. Set position_bias to simulate an order-sensitive model; the fuzzing guide shows decguard fuzz catching it.

Released under the Apache-2.0 License.