> ## Documentation Index
> Fetch the complete documentation index at: https://docs.irisagent.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Iris Control: LLM control plane

> Route, scope, check, and log your apps' LLM calls through one OpenAI-compatible endpoint.

## Overview

Iris Control sits between your applications and your LLM providers. Your apps call one OpenAI-compatible endpoint, and Iris Control applies your policy to every call:

* **Routing and failover:** each use case has a primary model and fallbacks. If a provider errors or times out, the next model answers.
* **Data scoping:** retrieval is limited to the call's product or tenant.
* **Tool limits:** each workflow can call only the tools you allow.
* **Grounding checks:** answers are checked against approved sources. Unsupported answers are flagged or replaced with your handoff message.
* **Call log and evals:** every prompt, answer, model, and policy decision is logged, and [AutoEval](https://docs.irisagent.com/autoeval.md) grades each output.

You can use AutoEval without routing traffic through Iris Control. See [Integration options](#integration-options).

> [!TIP]
>
> The [Iris Control live demo](https://irisagent.com/iris-control-demo/) runs failover, data scoping, and grounding checks on live models, with no signup.

## How it works

1. Your app sends an OpenAI-compatible request. The `model` field names a **use case** (for example `support-answers`), not a model.
2. Iris Control applies that use case's policy: scoped retrieval, allowed tools, and the model chain.
3. The primary model answers. If its provider fails, the next model in the chain is tried.
4. If the use case has grounding checks on, the answer is checked.
5. The answer comes back in the standard OpenAI format, with headers showing which model answered and what the policy did. The call is written to the call log.

## Integration options

Start with the lightest option. You can move down the table later without redoing setup.

| Option | What you send | On your request path? | What you get |
| - | - | - | - |
| Sample eval | 10 to 20 example calls (input and output) as a JSONL file | No | A sample AutoEval report: correctness and rule results, and a comparison with other models on the same inputs, with price |
| Log export | A recurring export of your LLM call logs, in the [AutoEval log format](https://docs.irisagent.com/autoeval.md#send-your-call-logs) | No | AutoEval grades, reports, and model comparisons on your own calls |
| Endpoint | Your OpenAI-compatible client pointed at Iris Control | Yes | Everything above, plus routing, failover, data scoping, tool limits, and grounding checks |

### Architecture

* **Server side only.** Call Iris Control from your backend. There is no client-side SDK, and API keys must never ship in client code.
* **No SDK to install.** The endpoint option uses your existing OpenAI client library. Log import and sample evals need no code.
* **Latency.** Log import and sample evals add none. The endpoint option adds one network hop plus any policy steps you turn on. Retrieval and grounding checks add the most, so turn them on per use case.
* **Deployment.** Hosted, or inside your own VPC when prompts and logs can't leave your environment. Prompts and logs are redacted for PII and secrets before they are stored.

## Quickstart

### Step 1: Get an API key

Your Iris Control point of contact provides your API key. Use it from your backend only.

### Step 2: Add your model providers

In the Iris Control dashboard, open **Providers** and add the ones you use: OpenAI, Anthropic, Google Gemini, Azure OpenAI, AWS Bedrock, or a self-hosted OpenAI-compatible model. Provider keys are stored encrypted and never returned by the API.

### Step 3: Create a use case

Open **Use cases** and create one per LLM feature, such as `support-answers` or `invoice-agent`. Set its model chain and, as needed, data scoping, tools, and grounding checks. See [Use case policies](#use-case-policies).

### Step 4: Point your client at Iris Control

In your existing OpenAI client, change the base URL and API key. Set `model` to the use case name.

```python Python theme={null}
from openai import OpenAI

client = OpenAI(
    base_url="https://api1.irisagent.com/v1/control",
    api_key=IRIS_API_KEY,
)

response = client.chat.completions.create(
    model="support-answers",
    messages=[{"role": "user", "content": "How do I export last month's invoices?"}],
    extra_headers={
        "X-Iris-Product": "billing",
        "X-Iris-Tenant": "acme-corp",
    },
)

print(response.choices[0].message.content)
```

```javascript Node.js theme={null}
import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://api1.irisagent.com/v1/control",
  apiKey: process.env.IRIS_API_KEY,
});

const response = await client.chat.completions.create(
  {
    model: "support-answers",
    messages: [{ role: "user", content: "How do I export last month's invoices?" }],
  },
  {
    headers: {
      "X-Iris-Product": "billing",
      "X-Iris-Tenant": "acme-corp",
    },
  }
);

console.log(response.choices[0].message.content);
```

```bash cURL theme={null}
curl https://api1.irisagent.com/v1/control/chat/completions \
  -H "Authorization: Bearer $IRIS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "X-Iris-Product: billing" \
  -H "X-Iris-Tenant: acme-corp" \
  -d '{
    "model": "support-answers",
    "messages": [{"role": "user", "content": "How do I export last month'\''s invoices?"}]
  }'
```

Streaming (`stream: true`), tool calling, and structured outputs work as they do with the OpenAI API.

## Request headers

| Header | Required | Description |
| - | - | - |
| `Authorization` | Yes | `Bearer` followed by your Iris Control API key. |
| `X-Iris-Product` | No | The product the call belongs to. Limits retrieval to knowledge tagged with this product. |
| `X-Iris-Tenant` | No | Your customer or tenant ID. Limits retrieval to that tenant's knowledge and groups the call log by tenant. |
| `X-Iris-Workflow` | No | The workflow or agent making the call. Selects the tool allowlist for that workflow. |
| `X-Iris-Trace-Id` | No | Your own request or trace ID, stored with the call log entry so you can join it to your logs. |

## Response headers

| Header | Description |
| - | - |
| `X-Iris-Request-Id` | The call log ID for this request. |
| `X-Iris-Model` | The model that produced the answer. |
| `X-Iris-Attempts` | The number of models tried. `2` means the primary failed and the first fallback answered. |
| `X-Iris-Grounding` | `passed`, `flagged`, `blocked`, or `off`. |
| `X-Iris-Tools-Removed` | Tools removed from the request or response because the workflow is not allowed to call them. |

## Use case policies

Each use case has one policy. Edit it in the dashboard, or export and import it as YAML.

```yaml theme={null}
use_case: support-answers
models:
  chain: [gpt-5.6-sol, grok-4.5, gemini-3.7-flash]
  attempt_timeout_seconds: 15
retrieval:
  enabled: true
  scope_by: [product, tenant]
  max_sources: 5
tools:
  workflows:
    billing-agent: [lookup_invoice, send_receipt]
    triage-agent: [lookup_account]
grounding:
  mode: block            # off, flag, or block
  check_links: true
  handoff_message: "Let me connect you with a teammate who can confirm this."
logging:
  store_prompts: true
  retention_days: 90
```

### Models and failover

`models.chain` lists models in order. Iris Control calls the first. On a provider error, a rate limit, or a timeout longer than `attempt_timeout_seconds`, it retries on the next one. Each attempt is logged with its model, outcome, and latency.

To change models, edit the chain. Your app code doesn't change, because it only names the use case.

### Data scoping

When `retrieval.enabled` is on, Iris Control searches your connected knowledge and adds matching passages to the request. Knowledge comes from your [data sources](https://docs.irisagent.com/data-sources/Zendesk.md), such as help centers, Confluence, websites, and [uploaded content](https://docs.irisagent.com/data-sources/Upload-Content.md).

`retrieval.scope_by` sets which headers limit the search:

* `product`: only knowledge tagged with the `X-Iris-Product` value is searched. Articles can belong to several products.
* `tenant`: only knowledge tagged with the `X-Iris-Tenant` value is searched.

If a scope header matches no knowledge, no passages are added.

**If you run your own retrieval**, leave `retrieval.enabled` off and send your passages in `iris.sources`. Iris Control uses them for the grounding check:

```python theme={null}
response = client.chat.completions.create(
    model="support-answers",
    messages=messages,
    extra_body={
        "iris": {
            "sources": [
                {
                    "title": "Exporting invoices",
                    "url": "https://help.example.com/invoices/export",
                    "body": "Go to Billing, then Invoice history, and select Export.",
                }
            ]
        }
    },
)
```

### Tool limits

`tools.workflows` maps each workflow to the tools it may call. For a request with `X-Iris-Workflow`:

* Tools not on the workflow's list are removed from the request before the model sees them.
* Calls to tools not on the list are removed from the response.

Removals are listed in `X-Iris-Tools-Removed` and in the call log. A request without `X-Iris-Workflow` keeps all its tools.

### Grounding checks

The grounding check runs after the model answers, against the call's sources (retrieved by Iris Control or sent in `iris.sources`):

* **Link check:** every link in the answer must point to a source. Others are reported as unapproved.
* **Claim check:** a judge model checks each claim against the sources and returns a verdict, a grounding score from 0 to 5, and any unsupported claims.

`grounding.mode` sets what happens when an answer fails either check:

| Mode | Behavior |
| - | - |
| `off` | No check runs. `X-Iris-Grounding: off`. |
| `flag` | Answer returned unchanged with `X-Iris-Grounding: flagged`. Use this to review results before blocking. |
| `block` | Answer replaced with `handoff_message`, with `X-Iris-Grounding: blocked`, so your app can route the user to a person. |

## Grounding check API

To check answers produced outside Iris Control, call the grounding check directly. Send up to 5 sources per request.

```bash theme={null}
curl https://api1.irisagent.com/v1/control/grounding-check \
  -H "Authorization: Bearer $IRIS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "question": "How long does a refund for a duplicate charge take?",
    "answer": "Duplicate charges are refunded within 24 hours. See https://example.com/credit-policy.",
    "sources": [
      {
        "title": "Refunds for duplicate charges",
        "url": "https://help.example.com/refunds",
        "body": "Once we confirm the duplicate charge, we refund it to the original payment method within 5 business days."
      }
    ]
  }'
```

Response:

```json theme={null}
{
  "passed": false,
  "urlCheck": {
    "approved": [],
    "unapproved": ["https://example.com/credit-policy"]
  },
  "judge": {
    "model": "gemini-3.7-flash",
    "verdict": "defect",
    "failureType": "hallucination",
    "groundingScore": 1,
    "unsupportedClaims": ["Duplicate charges are refunded within 24 hours"],
    "explanation": "The source says refunds take up to 5 business days."
  }
}
```

| Field | Description |
| - | - |
| `passed` | `true` when every link is approved and the judge's verdict is `correct`. |
| `urlCheck.approved` / `urlCheck.unapproved` | Links in the answer that do and don't match a source. |
| `judge.verdict` | `correct` or `defect`. |
| `judge.failureType` | For a defect: `hallucination`, `wrong_article`, `incomplete`, or `kb_gap`. |
| `judge.groundingScore` | 0 to 5. Higher means better supported by the sources. |
| `judge.unsupportedClaims` | Claims in the answer the sources don't support. |

## Call log

Every call appears under **Call log** in the dashboard, with:

* The use case, product, tenant, workflow, and your `X-Iris-Trace-Id`
* Each model attempt, with its outcome and latency
* The prompt, the retrieved or supplied sources, and the answer
* Tools removed by policy
* The grounding check result

Filter by use case, tenant, model, or grounding result, and export as CSV. Set retention with `logging.retention_days`, or turn off `logging.store_prompts` to keep only metadata.

## AutoEval

AutoEval grades call log outputs for correctness against their own prompts and against rules you write. You can replay the same inputs on other models and compare grades, cost, and latency before changing a model chain. It reads the call log, so it adds nothing to the request path. See [AutoEval](https://docs.irisagent.com/autoeval.md).

## Errors

Errors use the OpenAI error format.

| Status | Meaning |
| - | - |
| `400` | The request is malformed, or `model` doesn't name a use case. |
| `401` | The API key is missing or invalid. |
| `404` | The use case doesn't exist. |
| `429` | You've exceeded your rate limit. Retry after the `Retry-After` header. |
| `502` | Every model in the use case's chain failed. The error body lists each attempt. |

A blocked grounding check is not an error: it returns `200` with the handoff message and `X-Iris-Grounding: blocked`.

> [!NOTE]
>
> **Questions?** Contact us at [contact@irisagent.com](mailto:contact@irisagent.com) or [book an Iris Control demo](https://irisagent.com/get-a-demo-iris-control/).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.