{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://raw.githubusercontent.com/api-evangelist/knoxcall/main/json-schema/knoxcall-ai-gateway-agent-schema.json",
  "title": "AiGatewayAgent",
  "description": "An agent under a gateway — the addressable unit an AI SDK points at.",
  "x-generated": "2026-10-18",
  "x-method": "derived",
  "x-generator": "derive-json-schema.py",
  "x-source": "openapi/knoxcall-openapi.json#/components/schemas/AiGatewayAgent",
  "type": "object",
  "properties": {
    "id": {
      "type": "string",
      "format": "uuid"
    },
    "tenant_id": {
      "type": "string",
      "format": "uuid"
    },
    "gateway_id": {
      "type": "string",
      "format": "uuid"
    },
    "name": {
      "type": "string"
    },
    "slug": {
      "type": "string"
    },
    "description": {
      "type": [
        "string",
        "null"
      ]
    },
    "primary_route_id": {
      "type": [
        "string",
        "null"
      ],
      "format": "uuid"
    },
    "fallback_route_ids": {
      "type": "array",
      "items": {
        "type": "string",
        "format": "uuid"
      }
    },
    "model_allowlist": {
      "type": "array",
      "items": {
        "type": "string"
      }
    },
    "model_denylist": {
      "type": "array",
      "items": {
        "type": "string"
      }
    },
    "default_model": {
      "type": [
        "string",
        "null"
      ]
    },
    "model_rewrite": {
      "type": "object",
      "additionalProperties": {
        "type": "string"
      },
      "maxProperties": 100,
      "description": "Map of requested model → substituted model. Values must be STRINGS (the server validates with a string map) and at most 100 entries."
    },
    "budget_daily_usd": {
      "type": [
        "number",
        "null"
      ]
    },
    "budget_monthly_usd": {
      "type": [
        "number",
        "null"
      ]
    },
    "budget_per_call_max_tokens": {
      "type": [
        "integer",
        "null"
      ]
    },
    "budget_overage_action": {
      "type": "string"
    },
    "fallback_agent_id": {
      "type": [
        "string",
        "null"
      ],
      "format": "uuid"
    },
    "pii_redact_policy_id": {
      "type": [
        "string",
        "null"
      ],
      "format": "uuid"
    },
    "pii_detokenize_response": {
      "type": "boolean",
      "readOnly": true,
      "description": "AIGW-100 legacy alias. Read-only mirror of `pii_response_mode == \"detokenize\"`; the column is GENERATED in the database so the two can never disagree. Still ACCEPTED on create and update, where it is folded onto `pii_response_mode`."
    },
    "pii_request_mode": {
      "type": "string",
      "enum": [
        "off",
        "tokenize"
      ],
      "description": "What happens to the PROMPT before it leaves KnoxCall. `tokenize` (default) replaces detected entities with conversation-scoped, format-preserving tokens before the provider sees them; `off` forwards the prompt exactly as sent. Independent of `pii_response_mode` — before AIGW-100 request tokenization was gated on the response toggle, so an agent with a PII policy and `pii_detokenize_response: false` sent prompts verbatim."
    },
    "pii_response_mode": {
      "type": "string",
      "enum": [
        "redact",
        "detokenize"
      ],
      "description": "What happens to the provider's answer. `redact` runs the detector stack (fresh PII in the completion, canary tokens) and leaves `KC_*` tokens as tokens; `detokenize` (default) also swaps them back to this conversation's originals. There is no `off` — the detector stack runs on every 2xx."
    },
    "pii_streaming_holdback_chars": {
      "type": "integer"
    },
    "pii_streaming_mode": {
      "type": "string"
    },
    "cache_mode": {
      "type": "string"
    },
    "cache_ttl_seconds": {
      "type": "integer"
    },
    "cache_similarity_threshold": {
      "type": [
        "number",
        "null"
      ]
    },
    "cache_embedding_model": {
      "type": [
        "string",
        "null"
      ]
    },
    "streaming_enabled": {
      "type": "boolean"
    },
    "firewall_policy_id": {
      "type": [
        "string",
        "null"
      ],
      "format": "uuid"
    },
    "tool_allowlist": {
      "type": "array",
      "items": {
        "type": "string"
      }
    },
    "output_schema": {
      "type": [
        "object",
        "null"
      ],
      "additionalProperties": true
    },
    "output_validation_action": {
      "type": "string"
    },
    "data_residency_region": {
      "type": [
        "string",
        "null"
      ],
      "enum": [
        "us",
        "eu",
        "uk",
        "ca",
        "au",
        "jp",
        "in",
        null
      ],
      "description": "AIGW-53: pin this agent's upstream egress to a data-residency region. When set, a request whose upstream endpoint KnoxCall cannot classify as serving that region is refused on the data plane with HTTP 403 `residency_violation` — including a fallback route, which is where an unpinned failover would otherwise leave the region. `null` (the default) applies no residency constraint. See https://docs.knoxcall.com/ai-gateway/data-residency for the endpoints KnoxCall classifies and for what this control does and does not guarantee."
    },
    "cmek_key_id": {
      "type": [
        "string",
        "null"
      ],
      "format": "uuid",
      "description": "AIGW-53: the `tenant_master_keys` id this agent's AI data (its PII token map) is encrypted under. Must be one of THIS tenant's keys and must be customer-managed (`wrap_method = customer_kms`, i.e. BYOK configured) — a KnoxCall-wrapped key is refused, because a CMEK the platform can unwrap is not a CMEK. If the key is missing, retired, revoked, or your KMS refuses to unwrap it, the data-plane request is refused with HTTP 503 `cmek_unavailable` rather than written under a different key."
    },
    "status": {
      "type": "string",
      "enum": [
        "active",
        "paused",
        "archived"
      ]
    },
    "paused_reason": {
      "type": [
        "string",
        "null"
      ]
    },
    "tags": {
      "type": "object",
      "additionalProperties": true,
      "description": "FinOps attribution labels (cost_center/team/project/…) echoed onto this agent's usage rows."
    },
    "provider": {
      "type": [
        "string",
        "null"
      ],
      "description": "The upstream shape this agent fronts — one of the values `CreateAiGatewayAgentRequest.provider` enumerates (the catalog has grown to fourteen; `ai_gateway_agents_provider_check` is the authority). Set once, at create time, and deliberately NOT patchable: changing it without re-pointing `primary_route_id` at a matching route would make the stored value a lie. `null` on an agent created before the field existed whose route shape the backfill did not recognise — render that as \"custom\", never as a guess."
    },
    "routing_policy": {
      "allOf": [
        {
          "$ref": "#/$defs/AiGatewayRoutingPolicy"
        }
      ],
      "description": "Returned on every agent read. `{}` — the default for every agent — means one attempt per route and fail-over to `fallback_route_ids` on 5xx only.\n"
    },
    "guardrail_webhook_url": {
      "type": [
        "string",
        "null"
      ],
      "format": "uri",
      "description": "The external scanner this agent POSTs to for a verdict, or null when it has none. See `CreateAiGatewayAgentRequest.guardrail_webhook_url` for what the hook receives and how the destination is refused.\n"
    },
    "guardrail_webhook_secret_id": {
      "type": [
        "string",
        "null"
      ],
      "format": "uuid",
      "description": "Secret holding the HMAC-SHA256 key deliveries are signed with, or null when deliveries are sent unsigned.\n"
    },
    "guardrail_webhook_mode": {
      "type": "string",
      "enum": [
        "off",
        "request",
        "response",
        "both"
      ],
      "description": "Which directions are inspected. `off` when the agent has no hook — the server returns the string, never a boolean.\n"
    },
    "guardrail_webhook_timeout_ms": {
      "type": "integer",
      "minimum": 100,
      "maximum": 10000,
      "description": "How long the gateway waits for a verdict. Defaults to 2000."
    },
    "guardrail_webhook_failure_action": {
      "type": "string",
      "enum": [
        "fail_open",
        "fail_closed"
      ],
      "description": "What happens when the hook does not answer. Defaults to `fail_open`, the posture an agent already has with no hook at all.\n"
    },
    "created_at": {
      "type": "string",
      "format": "date-time"
    },
    "updated_at": {
      "type": "string",
      "format": "date-time"
    },
    "created_by": {
      "type": [
        "string",
        "null"
      ]
    },
    "agent_url": {
      "type": "string",
      "description": "The data-plane base URL for this agent — `https://{tenant}.knoxcall.com/v1/ai/{slug}`.\nPoint an AI SDK's `base_url` here and give it a capability token as the API key.\n\nPresent on EVERY agent projection: create, get, update and the list rows (AIGW-160\nera it was create and get only, so a list row was shaped differently from the row\ncreate returned). It is server-computed rather than stored, so it MOVES when `slug`\nchanges — which is why `PATCH` returns it too.\n\nAn EMPTY STRING when the tenant slug cannot be resolved. Treat empty as \"not\navailable\", never as a URL.\n"
    }
  },
  "required": [
    "id",
    "gateway_id",
    "name",
    "slug",
    "status",
    "agent_url",
    "created_at",
    "updated_at"
  ],
  "$defs": {
    "AiGatewayRoutingPolicy": {
      "type": "object",
      "description": "How this agent retries and spreads load (AIGW-42). An empty object — the default for every agent — means one attempt per route and fail-over to `fallback_route_ids` on 5xx only, which is the behaviour the gateway has always had.\n\nEvery field is clamped server-side. A request parked waiting to retry occupies an API worker, so `max_attempts` caps at 5, any single delay at 30s, a honoured `Retry-After` at 60s, and the total sleep for one request at 60s regardless of how the fields combine.\n",
      "additionalProperties": false,
      "properties": {
        "retry_on": {
          "type": "array",
          "description": "Which outcomes are retried on the SAME route before failing over. `429` also promotes a rate-limit response to a fail-over trigger; without it a 429 is returned to you immediately, unchanged.\n",
          "items": {
            "type": "string",
            "enum": [
              "429",
              "5xx",
              "timeout"
            ]
          }
        },
        "max_attempts": {
          "type": "integer",
          "minimum": 1,
          "maximum": 5,
          "description": "Attempts per candidate route, including the first. Forced to 1 when `retry_on` is empty, so a stored policy never claims a retry that cannot happen.\n"
        },
        "backoff_ms": {
          "type": "integer",
          "minimum": 0,
          "maximum": 30000,
          "description": "Delay before the second attempt."
        },
        "backoff_multiplier": {
          "type": "number",
          "minimum": 1,
          "maximum": 10
        },
        "max_backoff_ms": {
          "type": "integer",
          "minimum": 0,
          "maximum": 30000
        },
        "respect_retry_after": {
          "type": "boolean",
          "description": "Default true. An upstream `Retry-After` is honoured when it asks for LONGER than the computed backoff — it can extend a wait, never shorten one.\n"
        },
        "max_retry_after_ms": {
          "type": "integer",
          "minimum": 0,
          "maximum": 60000,
          "description": "Ceiling on a honoured `Retry-After`."
        },
        "jitter": {
          "type": "boolean",
          "description": "Default true. Spreads each delay across 50–100% of its computed value so a fleet does not retry in lockstep.\n"
        },
        "route_weights": {
          "type": "object",
          "description": "`{route_id: weight}`. When present, the route that serves a request is chosen in proportion to its weight across the primary and every fallback, and the remainder stay in weighted order for fail-over. A route not named here keeps weight 1; a route weighted 0 is drained and receives no traffic at all, including fail-over traffic. The token's own capability scope is enforced on every candidate, so a weight can never reach a route the token was not granted.\n",
          "additionalProperties": {
            "type": "integer",
            "minimum": 0,
            "maximum": 1000
          }
        }
      }
    }
  }
}