# Responses

Generate text or function calls with your project's controls applied to input and
generated tool calls. **The input scan is built into `POST /v1/responses`.**
Send one request with a `run:write` key; you do not need to call `POST /v1/scan/prompt`
separately or grant the key `scan:write`. Zaun assesses the submitted conversation,
applies the project's controls, and includes the scan result in `zaun.scan` on the
response. Generated function calls are also scanned before release. Plain assistant
output text is not scanned automatically. The built-in assessments are billed separately
from model generation at the scan rate described below.

`POST /v1/responses` accepts a text prompt or a conversation of input items, and
supports Claude Fable 5.1, Opus 5.5 and Sonnet 5, Cerebras GPT-OSS 120B
(`cerebras-gpt-oss-120b`), and GPT-6 Astra, Sol and Luna (`gpt-6-astra`, `gpt-6-sol`,
`gpt-6-luna`). Use `GET /v1/models` to check which are enabled on your deployment.

Responses are stateless. Send the full conversation in `input` on every request; `store: true` and
`previous_response_id` are rejected, and responses are not kept for later retrieval.

## Request flow

1. Validate the request and run input regex controls. A regex Block stops the request;
   Redact replaces matching input before it reaches the generation model.
2. If regex checks have no matches, start input classification and generation in
   parallel. If they match, finish the input assessment before generation.
3. Hold all generated content until the persisted input verdict permits release.
   A Block verdict cancels generation and returns an error before streaming begins.
4. Release buffered text, then stream new text as it arrives. Plain output text is
   not scanned automatically.
5. Buffer generated tool calls separately and scan them before release. A tool-call
   Block withholds those calls; text already streamed remains visible.

Failed safety checks prevent release of protected content. Non-streaming responses wait for the required
checks too. See [Project controls](project-controls.md) to configure the actions.

## Create a key

In **API keys**, select **Create key**, then enable **Run responses and list models**.
This grants `run:write`. Existing keys retain their permissions; create a replacement
if yours only has `scan:write`. Add **Read results** for assessment and session retrieval.
Keep API keys on your server, never in browser code.

## Send a request

```sh
curl https://api.zaun.ai/v1/responses \
  -H "Authorization: Bearer $ZAUN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "claude-sonnet-5",
    "instructions": "Answer clearly and briefly.",
    "input": "Why do rainbows form?",
    "max_output_tokens": 256,
    "sample": true
  }'
```

The response is a `response` object. Generated text is in `output` as a `message` item with
`output_text` content, and allowed tool calls are `function_call` items. The same response carries
the input assessment in `zaun.scan`, its enforced action in `verdict` and `zaun.enforcement`,
and a server-generated `request_uuid`. If the model generates function calls, their separate
assessment appears in `zaun.output_scan`. Plain text output has no `zaun.output_scan`.
Streaming responses include these fields in the final event. Each HTTP request starts a new operation.

`zaun.scan` is the full input scan result, with `status`, `violation`, `verdict`,
`findings`, `confidence`, `coverage`, and assessment `usage`, the input tokens billed
for it at the scan rate (`zaun.output_scan.usage` does the same for a tool-call
assessment). It gives you the scan decision in the same request as the generated
response. The top-level `metadata` field is reserved for ordinary Responses metadata,
so the scan result stays in the `zaun` extension rather than being copied into `metadata`.

For example, these fields appear alongside the generated `output` in a successful response:

```json
{
  "verdict": "no_action",
  "zaun": {
    "scan_uuid": "00000000-0000-4000-8000-000000000005",
    "scan": {
      "status": "completed",
      "violation": false,
      "verdict": "no_action",
      "findings": []
    },
    "enforcement": "allowed"
  }
}
```

`instructions` becomes the first system message. For a conversation, pass `input` as a list of
items instead of a string. No `zaun` request field is needed to enable scanning. Optionally,
`zaun.scan.session_uuid` links activity for later retrieval; it does not load conversation history. Generate one UUID per conversation on your side and reuse it on every call; Zaun never assigns one.

## Use the OpenAI SDK

Set the base URL to `https://api.zaun.ai/v1` and use a project key with `run:write`.

```python
import os
from openai import OpenAI

client = OpenAI(api_key=os.environ["ZAUN_API_KEY"], base_url="https://api.zaun.ai/v1", max_retries=0)

response = client.responses.create(
    model="claude-sonnet-5",
    instructions="Answer clearly and briefly.",
    input="Why do rainbows form?",
    max_output_tokens=256,
)
print(response.output_text)
scan = response.model_extra["zaun"]["scan"]
print(scan["status"], scan["violation"], scan["verdict"], scan["findings"])

stream = client.responses.create(
    model="claude-sonnet-5", input="Explain least privilege.", stream=True,
)
for event in stream:
    if event.type == "response.output_text.delta":
        print(event.delta, end="")
    elif event.type in ("response.completed", "response.incomplete"):
        print("\n", event.response.usage)
        print(event.response.model_extra["zaun"]["scan"])
```

The OpenAI Python SDK exposes Zaun's custom response field through
`response.model_extra["zaun"]`. For a generated function call, read its separate
assessment at `response.model_extra["zaun"]["output_scan"]`. A blocked input returns
HTTP 403 instead of a successful Response object; a regex Block can stop before
classifier assessment, so that error has no `zaun.scan`.

When input regex checks have no matches, generation and input classification run in
parallel. No response content is released until the input assessment is persisted
and allows the request. Buffered text is then released immediately, followed by
live `response.output_text.delta` events as the model generates them. Plain output text is not scanned.
Generated tool calls remain buffered until their separate assessment allows release.

Streams start with `response.created` and `response.in_progress` and end with `response.completed`,
or `response.incomplete` when the output budget runs out (`max_output_tokens`) or a control withholds
output (`content_filter`). The final event carries the full response, including usage. Every event has
an increasing `sequence_number`. Input blocks return HTTP 403 before streaming. Tool-call blocks end
the stream incomplete with `content_filter` and no tool calls; any text already streamed is retained.
An error after streaming begins is an `error` event, not a new HTTP status.

## Function tools

Pass `tools` and optionally `tool_choice`. Choices are `auto`, `required`, `none`, or a named
function (`{"type": "function", "name": "..."}`). The default is `auto` when tools are present.
Zaun returns tool requests; your application decides whether and how to execute them.

```python
tools = [{
    "type": "function",
    "name": "get_weather",
    "description": "Look up the weather for a city.",
    "parameters": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]},
}]
conversation = [{"role": "user", "content": "Find the weather in Seattle."}]
response = client.responses.create(
    model="claude-sonnet-5", input=conversation, tools=tools, tool_choice="required",
)
calls = [item for item in response.output if item.type == "function_call"]
for call in calls:
    print(call.call_id, call.name, call.arguments)

conversation += [call.model_dump(exclude_none=True) for call in calls]
conversation += [{"type": "function_call_output", "call_id": call.call_id, "output": "Rain, 12 C"} for call in calls]
followup = client.responses.create(model="claude-sonnet-5", input=conversation, tools=tools)
print(followup.output_text)
```

Allowed calls appear as `function_call` output items. Arguments are JSON strings. For a follow-up,
append each call item and one `function_call_output` item per call, with its `call_id` and your result
text, then resend the tool definitions and full conversation. Use `auto`, `required`, or a named choice
for tool-result follow-ups; `none` is supported only without tool-result history. Unknown, duplicate,
missing, or unmatched call IDs are rejected. Calls are never executed by Zaun.

### Web search as a function tool

Zaun has no built-in web search. Declare search as an ordinary function tool, run the
search yourself, and return the results as a `function_call_output`. Zaun assesses the
generated call before releasing it, and assesses the returned results as input on the
follow-up request, so web content passes through your project's controls before the
model reads it.

```python
tools = [{
    "type": "function",
    "name": "web_search",
    "description": "Search the web. Use for current events or facts newer than your training data.",
    "parameters": {
        "type": "object",
        "properties": {"query": {"type": "string", "description": "A short search query."}},
        "required": ["query"],
    },
}]
conversation = [{"role": "user", "content": "What did the Fed announce this week?"}]
response = client.responses.create(model="claude-sonnet-5", input=conversation, tools=tools)

for call in (item for item in response.output if item.type == "function_call"):
    query = json.loads(call.arguments)["query"]
    results = my_search(query)  # your search provider; return a compact text summary with URLs
    conversation.append(call.model_dump(exclude_none=True))
    conversation.append({"type": "function_call_output", "call_id": call.call_id, "output": results})

followup = client.responses.create(model="claude-sonnet-5", input=conversation, tools=tools)
print(followup.output_text)
```

Keep `output` compact: a few titles, URLs and snippets rather than whole pages. The
returned text counts toward the follow-up's input tokens for both the scan and the model.
If the model asks for several searches, loop until a response contains no `function_call`
items.

Only generated call names and arguments are output-scanned. Assistant prose and private reasoning are excluded.
For non-streaming responses, an output Block withholds calls and text and returns an incomplete
response with `incomplete_details.reason: content_filter`. Streaming text already released is not
retracted; blocked calls are never released. A failed tool-call scan returns an error. Streamed calls
arrive as `function_call` output items with `response.function_call_arguments.done`, after the same checks finish.
Redacted plain output remains in session activity with a `not_assessed` status.
Explicit scan requests always assess their submitted content, including assistant text.

## Supported features

| Field | Behavior |
| --- | --- |
| `model` | Required deployed alias. Discover available models with `GET /v1/models`. |
| `input` | Required. A string becomes one user message. A list may contain messages (`role` `user`, `assistant` or `system`, with text or `input_text` and `output_text` parts), `function_call` items and `function_call_output` items. Consecutive plain messages with the same role are supported. The first non-system message must be user, and the last item must be a user message or a function call output. |
| `instructions` | Optional system text, inserted before the conversation. |
| `max_output_tokens` | Optional model-specific output budget; default 512. Claude models allow up to 128,000 and Cerebras GPT-OSS 120B allows up to 16,384. |
| `temperature`, `top_p` | Omit both. These models use provider defaults. |
| `reasoning.effort` | Optional. One of `low`, `medium`, `high`, `xhigh` or `max`, limited to the levels in the model's `zaun.effort.levels` from `GET /v1/models`. Omit it to run at `zaun.effort.default`. Claude routes accept all five (Sonnet 5 and Fable 5.1 default `high`, Opus 5.5 defaults `medium`); Cerebras GPT-OSS accepts `low`, `medium` and `high` (default `medium`); the GPT-6 routes accept all five (Sol and Luna default `medium`; OpenAI does not document Astra default, so `zaun.effort.default` is null there). A level the model lacks, or any other `reasoning` field, returns 422 `invalid_request` with `param` `reasoning.effort` before any paid call. A non-streaming response must finish within 180 seconds and a stream must release its first text within 180 seconds, thinking included. Opus 5.5 at `max` ran past the first on staging and inside the second, so send `stream: true` at `high` and above and expect `xhigh` or `max` on hard prompts to fail with 504 `deadline_exceeded` when thinking alone outlasts the budget. |
| `tools`, `tool_choice` | Function tools only. Claude routes reject `strict: true`; explicit `strict: false` is treated as omission. Fable 5.1 and Opus 5.5 reject `required` and named choices with 422. See [Function tools](#function-tools). |
| `stream` | Set true for typed server-sent events. Use streaming for large output budgets and for high reasoning effort: a non-streaming response must finish within 180 seconds and a stream must release its first text within 180 seconds, or the request returns 504 `deadline_exceeded`; once text is flowing a stream may run for up to an hour. Generated tool calls are assessed before release. Plain text output is not scanned. |
| `store` | Only `false` is accepted. Responses are never stored. |
| `zaun.scan.session_uuid` | Optional UUID shared across related scans and responses. |
| `sample` | Defaults to true. With false, Zaun keeps the redacted text but hides it from you: it is not shown in the console or returned by session retrieval. It is still used for session context and campaign detection. A project stops all retention only by setting `retain_activity` to false, which also removes the text from historical investigation. Read `zaun.activity_capture.status` for `requested` or `disabled` (project retention off); requested capture is asynchronous. |

User messages may include PNG or JPEG `input_image` parts as data URLs, up to four per request,
on models that support vision. Zaun reads their text and assesses it with the message; the model
receives the image. Each image costs $0.0075 for text recognition. See [limits](limits.md).

Remote image URLs, files, audio, built-in tools, developer-role messages, reasoning summaries, text-format
options, `previous_response_id`, `metadata`, and provider-specific options are not supported. Unsupported
fields are rejected rather than silently ignored. Model discovery also requires `run:write`:

```sh
curl https://api.zaun.ai/v1/models -H "Authorization: Bearer $ZAUN_API_KEY"
```

## Cerebras GPT-OSS

Use `model="cerebras-gpt-oss-120b"`. The native model ID is
resolved internally. Input costs $0.35 per million tokens and output costs $0.85
per million tokens, including reasoning tokens reported by the model.

```python
response = client.responses.create(
    model="cerebras-gpt-oss-120b",
    input="Explain least privilege.",
    max_output_tokens=512,
)
```

## Availability and retries

When enabled, an explicit upstream throttle rejection can use one alternate route
to the same model. Timeouts, connection failures, safety blocks, and organization
limits do not authorize a fallback. The configured model's published customer
rate remains fixed for the request, including when the alternate route is used.
Input permission gates all output; only generated tool calls need an output assessment. No model substitution is performed. The
`cerebras-gpt-oss-120b` alternate route remains pinned to Cerebras.

## Input and output controls

Configure controls in **Projects**. The strongest matching action wins:
**Block**, then **Redact**, then **Alert**, then **No action**. Unconfigured controls default to No action.

Six deterministic regex controls protect **completion inputs** and all explicitly
submitted prompt, output and document scans:

- **Email address** (`pii_email`).
- **Phone number** (`pii_phone`): formatted domestic and international numbers.
- **Credit card number** (`pii_credit_card`): 13 to 19 digits with a valid Luhn checksum.
- **IP address** (`pii_ip`): valid IPv4 and IPv6 addresses.

- **Social Security numbers** (`pii_ssn`): US SSN patterns such as `123-45-6789`.
- **API keys and secrets** (`credentials`): recognized API keys, bearer tokens,
  password assignments, private keys, and credential-bearing URLs.

These checks cover model-visible messages, history, system text, and tool definitions.
Regex recognition is format-based; use the control descriptions for supported patterns. Stored findings contain only
control IDs and their selected action, never matched sensitive values.

A regex **Redact** replaces matching completion input with category markers before either classifier or generation sees it. The original request fingerprint still identifies retries; the dispatched invocation fingerprint reflects sanitized content. A separate Block still wins. Tool arguments remain valid JSON. If a sensitive match occurs in a structural identifier or schema key that cannot be safely rewritten, the request fails before dispatch.

A regex **Block** stops before generation or classifier calls. A regex **Alert** or
**No action** or **Redact** continues through input assessment before generation starts. With no
regex matches, generation runs alongside the input assessment. An input Block or
technical input-assessment failure discards buffered output and cancels local
provider consumption. A completed classifier abstention defaults to No action.

Generated tool calls are assessed before release. Tool Block withholds those calls.
For non-streaming requests, a tool Block returns no output items and
`incomplete_details.reason: content_filter`; streaming text already released is not retracted.
Tool assessment errors never release tool calls. Explicit scan requests retain
their submitted-content behavior and also evaluate regex controls. Prompt and output scans return optional `redacted_prompt` (rendered assessed text) and `input_checks`; document scans return regex evidence and the verdict, never redacted extracted text. Regex controls are separate from ATLAS technique labels.

Read `zaun.input_checks` for regex control IDs and action, `zaun.scan` for input
classification, and `zaun.output_scan` for tool-call classification. The latter is
omitted for plain text output. The top-level action includes regex findings.
The response also carries `zaun.enforcement`, `zaun.reason`, and
`zaun.accounting_status`. Regex-block errors include `zaun.input_checks` without
claiming a classifier assessment occurred.

## Limits and accounting

Responses share your organization's prompt-scan allowance across all keys and
projects. Each response consumes one request; input and generated tool-call assessments
consume protected tokens from the same allowance.

| Tier | Shared requests/min | Shared protected tokens/min |
| --- | --- | --- |
| 1 | 60 | 1,000,000 |
| 2 | 300 | 5,000,000 |
| 3 | 600 | 10,000,000 |

Responses follow the [request limits](limits.md). Discover each model's configured
output cap with `GET /v1/models`. A tool-call assessment that exceeds the available
token allowance withholds those calls and returns an error. Honor `Retry-After`
when retrying rate-limit or temporary availability errors.

Completed input and tool-call assessments cost $0.20 per million protected tokens,
and each reports the input tokens it was charged in its `usage`.
Generation is additionally billed at these token rates:

| Model alias | USD / million input tokens | USD / million output tokens |
| --- | ---: | ---: |
| `claude-fable-5-1` | $11.00 | $55.10 |
| `claude-opus-5-5` | $4.40 | $22.10 |
| `claude-sonnet-5` | $2.20 | $11.10 |
| `cerebras-gpt-oss-120b` | $0.35 | $0.85 |
| `gpt-6-astra` | $10.00 | $50.10 |
| `gpt-6-sol` | $2.00 | $10.10 |
| `gpt-6-luna` | $0.10 | $0.60 |

Use `GET /v1/models` for enabled model IDs and current token rates.
Claude Sonnet 5, Opus 5.5, and Fable 5.1 requests include five-minute provider
cache checkpoints after tools, system instructions, and the final conversation turn,
for both buffered and streaming responses. Keep reusable content at the beginning
of the request. Cache hits are best effort and depend on the provider's minimum
prefix length and routing; inspect `usage.input_tokens_details.cached_tokens` and
cache-write counts rather than assuming every repeat is cached. Cache reads and
writes use the model's published cache rates, including the write premium.
`sample=false` controls whether Zaun shows you the stored activity, not provider inference caching.
See [AWS prompt caching](https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-caching.html).
Batch and priority inference are not enabled by this endpoint. Provider output
counts include reasoning tokens, even when reasoning is not shown. `reasoning.effort`
changes how much the model thinks and writes, so it changes output cost at the same
per-token rate: a claim-judging call can run at `low` and a drafting call at `high` on
the same key. The effort travels to Bedrock as Anthropic's native `output_config.effort`,
to OpenAI's Responses API as `reasoning.effort`, and to Cerebras as `reasoning_effort`.
On an OpenRouter throttle fallback it is sent as OpenRouter's `reasoning.effort`; if that route cannot honor it the
request fails rather than silently running at another level.
`usage.output_tokens_details.reasoning_tokens` reports the provider's separate
reasoning count when available, or `null` when unavailable (including Bedrock
Converse). It is a subset of output tokens, not an additional charge. Unknown
reasoning usage is never presented as zero or estimated from generated text.

Before dispatch, credits are held using a conservative input budget and the requested
output cap. Settlement uses only actual provider-reported input and output counts.
Fractional microdollars carry across requests instead of rounding every request up.
Generation may incur cost before the input verdict is known. Generated tokens still incur cost if input or tool output is blocked or an assessment fails. Usage the provider already reported is charged when a request stops early, including when the input is blocked, an assessment fails, or a streaming client stops reading.
A request that never reaches the provider, or that the provider refuses before generating (such as a throttle with no alternate route), releases its hold without a generation charge.
Unknown provider usage is not guessed or charged. Interrupted calls with unknown usage retain their credit hold until usage is reconciled; they are never automatically repeated. `sample: false` does not make requests free.

## Retries and errors

| Response | Meaning |
| --- | --- |
| 401 or 403 `forbidden` | Check key permissions and project status. |
| 403 `input_block` or `policy_block` | A project control blocked the request. The decision is in `zaun`. |
| 402 | Insufficient credits for admission. |
| 409 | The same `Idempotency-Key` is in progress or already used, or its body changed. |
| 422 | Invalid model, content, or parameters. |
| 429 | Shared request or token allowance exhausted. Respect `Retry-After`. |
| 503 | Temporary service or concurrency capacity failure, or `assessment_unavailable` when the input assessment could not be trusted. Nothing was generated; respect `Retry-After`. |
| 504 | Deadline exceeded. The provider may already have been called. |
| 501 | Generation is disabled on the target deployment. |

Without `Idempotency-Key`, each HTTP request starts a new generation. Supply a stable
project-scoped key through the SDK `extra_headers` option for retries. Reusing the same
key and body returns 409 without another generation because output is not retained for
replay. A changed body returns `idempotency_conflict`. After a timeout or uncertain
response, retrying without the original key can generate and bill another answer.

## Activity and session retrieval

By default, redacted response inputs and outputs are queued after assessment and
retained in activity storage for 30 days. The Violations page shows flagged requests
and their available session timeline. Retrieval requires `result:read`:

```sh
curl "https://api.zaun.ai/v1/sessions/$SESSION_UUID?limit=10" \
  -H "Authorization: Bearer $ZAUN_API_KEY"
```

Activity appears asynchronously. Refresh for late arrivals; a session timeline
contains available captured activity and is not a complete audit log. Regex redaction
covers recognized patterns.

Successful responses distinguish two identifiers: `X-Request-Id` traces the HTTP request,
while top-level `request_uuid` identifies the persisted generation and its assessment.

## Explicit output scans

Use `POST /v1/scan/prompt` to assess output text and tool calls directly, without generating a response or executing tools. Supply assistant messages with `content` text and optional `tool_calls`, each `{"id", "type": "function", "function": {"name", "arguments"}}`. A tool-only message uses `content: null`; `function.arguments` is a JSON object encoded as a string. **Every supplied message is scanned. Only include messages you want scanned.**

```json
{
  "messages": [{
    "role": "assistant",
    "content": "Here is the requested summary.",
    "tool_calls": [{
      "id": "call_1",
      "type": "function",
      "function": {"name": "send_email", "arguments": "{\"to\":\"team@example.com\",\"body\":\"Summary\"}"}
    }]
  }],
  "sample": true
}
```

Requires a project key with `scan:write`. The response includes a server-generated `request_uuid`. It shares scan pricing and the organization's scan RPM and TPM allowance. The compact response includes the combined violation, configured action, detected techniques and usage. Regex controls apply to explicitly submitted output; a Redact match uses the existing `redacted_prompt` response field. Optional `session_uuid` attaches activity to a session; `sample: false` hides the stored activity from you.

Automatic response behavior is unchanged: ordinary generated text is not scanned, and tool calls are assessed before release.

See [scan feedback](project-controls.md#report-an-incorrect-detection) for assessment IDs and submitting a review without changing a verdict.

For `claude-opus-5-5`, use `tool_choice: "auto"` (the default) or `"none"`.
Bedrock does not support `"required"` or a named function choice for this model;
Zaun rejects them with HTTP 422 before billed work begins.
