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

# Presets

> Named, versioned bundles of model, sampling parameters, system prompt, and provider preferences — pin request configuration by slug and roll back to any prior snapshot from immutable version history.

A **preset** is a team-scoped, versioned bundle of a canonical model, sampling parameters, an optional system prompt, and provider preferences — addressed by a short slug like `support-bot`. Clients reference the slug at request time; the proxy resolves it to the current preset body before it dispatches upstream.

Every save creates an immutable snapshot in `preset_versions`, so you can diff, audit, or roll back a bad change without stitching state back together from request logs.

## When to use presets

Reach for a preset instead of hardcoding request fields when you want to:

* Pin one production configuration — model, temperature, system prompt, provider preferences — behind a stable name that clients don't need to redeploy to change.
* Ship a config change (a smarter model, a lower temperature, a rewritten system prompt) and be able to instantly point back at the previous snapshot if quality regresses.
* Keep an auditable history of who changed which field, and when, without carrying that history in your own database.

Aliases ([Routing & fallbacks](/routeshift/routing)) rewrite the model *name*. Presets rewrite the whole request body. Use aliases for a single-field indirection; use presets when the temperature, system prompt, or provider preferences should travel with the config.

## Anatomy

| Field            | Type           | Notes                                                                                                                          |
| ---------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `slug`           | string         | `^[a-z0-9][a-z0-9-]{0,63}$`. Unique per team. Immutable after create.                                                          |
| `version`        | integer        | Monotonic, starts at `1`, increments on every update.                                                                          |
| `model`          | string         | Must be a canonical name from the RouteShift model registry.                                                                   |
| `params`         | object         | Any subset of `temperature`, `max_tokens`, `top_p`, `frequency_penalty`, `presence_penalty`, `stop`. Unknown keys are dropped. |
| `system_prompt`  | string \| null | Optional. Prepended to the request's messages when the preset is resolved.                                                     |
| `provider_prefs` | object         | Provider-preference object (order, allow / deny lists, quantization filters).                                                  |
| `enabled`        | boolean        | Defaults to `true`. Disable a preset to make it fail closed without deleting the version history.                              |

Every preset row is scoped by `team_id`; a slug in team A and the same slug in team B are two independent presets. Cross-team reads return `preset_not_found` — never a distinguishable "wrong team" error.

## Reference a preset from the SDK

`@routeshift/sdk` accepts a `preset` slug on any `chat` or `chatStream` call. Because the preset carries its own canonical model, you can call the proxy without configuring a `defaultModel` on the client and without passing `model` on the request — the proxy resolves both from the preset.

```ts theme={null}
import { ProxyClient } from "@routeshift/sdk";

const client = new ProxyClient({
  baseUrl: "https://proxy.routeshift.io",
  apiKey: process.env.ROUTESHIFT_KEY!,
});

// Preset-only call — no model on the client, no model on the request.
const res = await client.chat({
  preset: "support-bot",
  messages: [{ role: "user", content: "How do I reset my password?" }],
});
```

The SDK will not merge its `defaultModel` into a request that carries a `preset`, so a client-side default won't accidentally override the preset's canonical model. Passing `model`, `models`, or a `:online` suffix alongside `preset` still works — those inputs win, exactly as they do on the raw HTTP endpoint.

If you call `chat` with neither a `preset`, a `model`, a `models` array, nor a configured `defaultModel`, the SDK throws before any network call:

```
model is required when no defaultModel is configured
```

Streaming works the same way:

```ts theme={null}
const stream = client.chatStream({
  preset: "support-bot",
  messages: [{ role: "user", content: "Draft a triage reply." }],
});

for await (const event of stream) {
  process.stdout.write(event.content ?? "");
}
```

## Manage presets

Open **Routing → Presets** in the dashboard, or drive the API directly. Only workspace admins can create, update, or delete presets. Any team member can read them.

### Create

```http theme={null}
POST /api/presets
Content-Type: application/json

{
  "slug": "support-bot",
  "model": "gpt-4o",
  "params": { "temperature": 0.2, "max_tokens": 1024 },
  "system_prompt": "You are a support agent for Acme…",
  "provider_prefs": { "order": ["openai", "azure"] },
  "enabled": true
}
```

Returns `{ "id": "preset_…", "slug": "support-bot", "version": 1, "proxy_cache_invalidated": true }` with `201`. On slug collision the API returns `409 preset_slug_taken`. Invalid slugs return `400 invalid_preset_slug`; unknown models return `400 invalid_preset_model`.

Every accepted write (`POST`, `PUT`, disable, delete) also invalidates the proxy's preset cache so the change takes effect on the next request. The response reports the outcome of that step explicitly:

* On success: `"proxy_cache_invalidated": true`.
* On failure: `"proxy_cache_invalidated": false`, `"proxy_cache_error": "proxy_cache_invalidation_failed"`, and `"cache_ttl_seconds"` — the database write still committed, but the proxy may keep serving the previous preset until the TTL elapses.

### Update

```http theme={null}
PUT /api/presets/support-bot
Content-Type: application/json

{
  "model": "gpt-4o",
  "params": { "temperature": 0.3, "max_tokens": 1024 },
  "system_prompt": "You are a support agent for Acme…",
  "provider_prefs": { "order": ["openai", "azure"] }
}
```

`PUT` is a **full-body replace**. `model`, `params`, `system_prompt`, and `provider_prefs` are all required — omit any and the request fails with `400 full_preset_body_required`. `enabled` is optional and preserves its current value when omitted.

Every accepted `PUT` bumps `version` by one and appends a `preset_versions` row. The response is `{ "slug": "support-bot", "version": 2, "proxy_cache_invalidated": true }` — see [Proxy cache invalidation](#proxy-cache-invalidation) for the failure shape.

### Delete or disable

```http theme={null}
DELETE /api/presets/support-bot            # remove the row and mark it not-found
DELETE /api/presets/support-bot?disable=true   # keep history, set enabled=false
```

Use `?disable=true` when you want the slug to fail closed but keep the version history discoverable. A hard `DELETE` removes the `presets` row (`preset_versions` rows persist for audit).

Both variants return `{ "ok": true, "disabled": <bool>, "proxy_cache_invalidated": true }` on success. If the proxy invalidation call fails, the database write still commits and the response reports [proxy cache invalidation](#proxy-cache-invalidation) explicitly.

## Proxy cache invalidation

After a successful create, publish, delete, or disable, the dashboard tells the proxy to drop its cached copy of the preset so the next request resolves the new body immediately. Every successful mutation response includes an explicit status for that side effect:

| Field                     | Type    | When present                                                                                                                                                                     |
| ------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `proxy_cache_invalidated` | boolean | Always. `true` when the proxy accepted the invalidation, `false` on any failure.                                                                                                 |
| `proxy_cache_error`       | string  | Only when `proxy_cache_invalidated` is `false`. Fixed value `proxy_cache_invalidation_failed`.                                                                                   |
| `cache_ttl_seconds`       | integer | Only when `proxy_cache_invalidated` is `false`. Currently `60` — the upper bound on how long stale routing can persist before the proxy TTL expires the cached entry on its own. |

The database write is **committed** before the invalidation call runs, so a `proxy_cache_invalidated: false` response still means the preset was saved. The proxy may serve the previous version for up to `cache_ttl_seconds` before it re-reads from the database.

```json theme={null}
{
  "slug": "support-bot",
  "version": 2,
  "proxy_cache_invalidated": false,
  "proxy_cache_error": "proxy_cache_invalidation_failed",
  "cache_ttl_seconds": 60
}
```

### How to handle it

* **Non-blocking clients** (dashboards, background jobs): treat the response as a success — the preset is saved, and the proxy will pick it up within `cache_ttl_seconds`.
* **Change-control flows** where new traffic must land on the new version immediately: surface `proxy_cache_error` to the operator and retry the same request (idempotent for `PUT`, `DELETE`, and `DELETE?disable=true`) until you see `proxy_cache_invalidated: true`, or wait out `cache_ttl_seconds` before declaring the rollout done.
* **`POST` retries**: the create already committed, so retrying with the same slug returns `409 preset_slug_taken`. To force invalidation after a failed `POST`, issue a `PUT` with the same body — it bumps the version and re-runs invalidation.

The proxy admin endpoint the dashboard calls is documented in [Admin API](/routeshift/api/admin); a `proxy_admin_secret_not_configured` response (`500`) means the dashboard is misconfigured and no mutations will invalidate the cache until the secret is set.

## Version history

Every accepted create and update writes an immutable snapshot to `preset_versions`. Snapshots are keyed by `(team_id, preset_id, version)` and carry the full body (`model`, `params`, `system_prompt`, `provider_prefs`) plus `created_by` and `created_at`.

<Note>
  Preset version history is scoped strictly to the caller's team. A request for `GET /api/presets/{slug}/versions` from a team that doesn't own `{slug}` returns the same `404 preset_not_found` response as a truly missing preset — teams can't probe the existence of another team's presets.
</Note>

### List snapshots

```http theme={null}
GET /api/presets/{slug}/versions
```

Returns every snapshot for the slug, newest first:

```json theme={null}
{
  "versions": [
    {
      "version": 3,
      "model": "gpt-4o",
      "params": { "temperature": 0.3, "max_tokens": 1024 },
      "system_prompt": "You are a support agent for Acme…",
      "provider_prefs": { "order": ["openai", "azure"] },
      "created_by": "user_01H…",
      "created_at": "2026-07-09T18:42:11.204Z"
    },
    {
      "version": 2,
      "model": "gpt-4o",
      "params": { "temperature": 0.2, "max_tokens": 1024 },
      "system_prompt": "You are a support agent for Acme…",
      "provider_prefs": { "order": ["openai"] },
      "created_by": "user_01H…",
      "created_at": "2026-06-30T14:10:03.612Z"
    }
  ]
}
```

### Read one snapshot

```http theme={null}
GET /api/presets/{slug}/versions/{version}
```

`{version}` must be a positive base-10 integer within the PostgreSQL 32-bit `integer` range (max `2147483647`). Leading zeros, negative values, decimals, and values outside the range return `400 invalid_preset_version` before any database lookup.

```json theme={null}
{
  "version": {
    "version": 2,
    "model": "gpt-4o",
    "params": { "temperature": 0.2, "max_tokens": 1024 },
    "system_prompt": "You are a support agent for Acme…",
    "provider_prefs": { "order": ["openai"] },
    "created_by": "user_01H…",
    "created_at": "2026-06-30T14:10:03.612Z"
  }
}
```

### Roll back to a previous version

There's no `POST /rollback` shortcut — read the snapshot you want and replay it as a `PUT`:

```bash theme={null}
# Fetch version 2, strip the audit fields, replay as an update.
curl -s https://app.routeshift.io/api/presets/support-bot/versions/2 \
  -H "Authorization: Bearer $ROUTESHIFT_SESSION" \
  | jq '.version | { model, params, system_prompt, provider_prefs }' \
  | curl -s -X PUT https://app.routeshift.io/api/presets/support-bot \
      -H "Authorization: Bearer $ROUTESHIFT_SESSION" \
      -H "Content-Type: application/json" \
      --data-binary @-
```

The result is a new snapshot (say `version = 4`) whose body is identical to version 2. History stays linear and append-only, which keeps the audit story simple — no version is ever mutated or removed.

## Errors

| Response                    | Status | When                                                                                            |
| --------------------------- | ------ | ----------------------------------------------------------------------------------------------- |
| `invalid_preset_slug`       | 400    | Slug doesn't match `^[a-z0-9][a-z0-9-]{0,63}$`.                                                 |
| `invalid_preset_model`      | 400    | Model is not in the canonical model registry.                                                   |
| `invalid_preset_params`     | 400    | `params` is not a JSON object.                                                                  |
| `invalid_preset_version`    | 400    | Version path segment is not a positive integer within `int4` range.                             |
| `full_preset_body_required` | 400    | `PUT` omitted one of `params`, `system_prompt`, or `provider_prefs`.                            |
| `preset_not_found`          | 404    | Slug doesn't exist in the caller's team — including when it exists in another team.             |
| `preset_slug_taken`         | 409    | `POST` with a slug already used by the team.                                                    |
| `Admin role required`       | 403    | Non-admin attempted a write. Reads only require team membership.                                |
| Demo write blocked          | 403    | Demo mode is read-only. Preset writes are blocked; reads still work against the demo workspace. |

## Demo mode

Preset **reads** (list, detail, version history) transparently substitute the demo team for the caller's team when demo mode is active — you see the demo workspace's presets and version history, exactly as they'd render in production. Preset **writes** (`POST`, `PUT`, `DELETE`) are blocked with `403` so a demo session can't mutate the real workspace.
