/* ============================================================
   NeuroRoute — Customer Docs hub.
   Logged-in customer nav page. Reference documentation for the topics a
   customer needs to self-serve integrate and configure: API usage, evals,
   routing, caching, compression, security/privacy, and other settings.

   Content lives in DOC_TOPICS below as plain data (no markdown/HTML in the
   strings — this file's block renderer turns {heading, paragraphs, list,
   table, code, note} into real elements). Keeping content as data rather
   than hand-written JSX per section makes each topic easy to review/update
   without touching rendering logic.
   ============================================================ */

var DOC_TOPICS = [
  {
    "id": "api",
    "title": "Using the API",
    "icon": "apikey",
    "summary": "NeuroRoute exposes an OpenAI-compatible Chat Completions endpoint, so existing OpenAI SDK code works by just pointing base_url at NeuroRoute and using an nr_ API key.",
    "sections": [
      {
        "heading": "Authentication",
        "paragraphs": [
          "Every request needs an nr_ API key, sent as a plain Authorization: Bearer header, exactly like an OpenAI API key. No request signing is required for this path.",
          "A key looks like nr_<env>_<key_id>.<secret>, for example nr_live_k_abc123.s3cr3t. NeuroRoute splits the token on the last period to recover the secret, verifies it against a stored hash, and attaches your organization, role, and any per-key defaults (routing strategy, model allowlist, budgets) to the request.",
          "An older HMAC-signed request path (X-NR-Key-ID / X-NR-Timestamp / X-NR-Nonce / X-NR-Signature headers) is also supported for callers that prefer request signing, but it is not required — the Bearer nr_... form is the standard, OpenAI-SDK-compatible way to authenticate."
        ]
      },
      {
        "heading": "Base URL and SDK compatibility",
        "paragraphs": [
          "The base URL is https://neuroroute.sislcloudworx.ai/v1. Because the Chat Completions surface matches OpenAI's wire format, any OpenAI SDK (Python, Node, etc.) works unmodified — just set base_url and api_key.",
          "The core endpoints are POST /v1/chat/completions (streaming and non-streaming), GET /v1/models, and POST /v1/feedback."
        ],
        "code": {
          "lang": "bash",
          "text": "curl https://neuroroute.sislcloudworx.ai/v1/chat/completions \\\n  -H \"Authorization: Bearer nr_live_k_abc123.yoursecret\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"model\": \"auto\",\n    \"messages\": [\n      {\"role\": \"user\", \"content\": \"Explain quantum entanglement in one paragraph.\"}\n    ]\n  }'"
        }
      },
      {
        "heading": "Python quickstart (openai SDK)",
        "paragraphs": [
          "Point the standard openai Python client at NeuroRoute's base URL and use your nr_ key as the API key. NeuroRoute-specific behavior (like picking a routing strategy for one call) is passed through extra_headers."
        ],
        "code": {
          "lang": "python",
          "text": "from openai import OpenAI\n\nclient = OpenAI(\n    base_url=\"https://neuroroute.sislcloudworx.ai/v1\",\n    api_key=\"nr_live_k_abc123.yoursecret\",\n)\n\nresponse = client.chat.completions.create(\n    model=\"auto\",\n    messages=[\n        {\"role\": \"user\", \"content\": \"Explain quantum entanglement in one paragraph.\"}\n    ],\n    extra_headers={\"X-Routing-Strategy\": \"cheapest\"},\n)\n\nprint(response.choices[0].message.content)\nprint(response.model)  # the model NeuroRoute actually routed to"
        }
      },
      {
        "heading": "Request body reference",
        "paragraphs": [
          "The request body matches OpenAI's Chat Completions format plus a small number of NeuroRoute extensions (noted below). Fields not listed here are not part of the request schema."
        ],
        "table": {
          "columns": [
            "Parameter",
            "Required",
            "Type",
            "Default",
            "What it does"
          ],
          "rows": [
            [
              "model",
              "Yes",
              "string",
              "—",
              "Model ID, or \"auto\" to let the router choose. A specific model ID bypasses routing entirely and calls that exact model directly."
            ],
            [
              "messages",
              "Yes",
              "array",
              "—",
              "OpenAI-style chat messages (system/user/assistant/tool roles). Content can be a plain string or multimodal content parts (text, image_url)."
            ],
            [
              "temperature",
              "No",
              "number",
              "provider default",
              "Sampling temperature."
            ],
            [
              "top_p",
              "No",
              "number",
              "provider default",
              "Nucleus sampling parameter."
            ],
            [
              "n",
              "No",
              "integer",
              "1",
              "Number of completions requested (accepted for OpenAI compatibility)."
            ],
            [
              "stream",
              "No",
              "boolean",
              "false",
              "Enables server-sent-events streaming of the response."
            ],
            [
              "stop",
              "No",
              "string or array of strings",
              "none",
              "One or more stop sequences."
            ],
            [
              "max_tokens",
              "No",
              "integer",
              "provider default",
              "Maximum output tokens (older-style field)."
            ],
            [
              "max_completion_tokens",
              "No",
              "integer",
              "provider default",
              "Maximum output tokens (newer-style field). Takes precedence over max_tokens when both are set."
            ],
            [
              "presence_penalty",
              "No",
              "number",
              "provider default",
              "Passed through to providers that support it."
            ],
            [
              "frequency_penalty",
              "No",
              "number",
              "provider default",
              "Passed through to providers that support it."
            ],
            [
              "logit_bias",
              "No",
              "object",
              "none",
              "Passed through to providers that support it."
            ],
            [
              "user",
              "No",
              "string",
              "none",
              "Opaque end-user identifier, passed through."
            ],
            [
              "tools",
              "No",
              "array",
              "none",
              "OpenAI-style tool/function definitions."
            ],
            [
              "tool_choice",
              "No",
              "string or object",
              "\"auto\"",
              "Controls whether/which tool the model should call."
            ],
            [
              "response_format",
              "No",
              "object",
              "{\"type\":\"text\"}",
              "Set {\"type\":\"json_object\"} to request JSON-mode output where the provider supports it."
            ],
            [
              "seed",
              "No",
              "integer",
              "none",
              "Passed through to providers that support deterministic sampling."
            ],
            [
              "stream_options",
              "No",
              "object",
              "none",
              "Set {\"include_usage\": true} to receive a final SSE chunk carrying token usage."
            ],
            [
              "parallel_tool_calls",
              "No",
              "boolean",
              "provider default",
              "Passed through to providers that support it."
            ],
            [
              "reasoning_effort",
              "No",
              "string",
              "none",
              "\"low\", \"medium\", or \"high\" — passed through to reasoning-capable OpenAI-compatible providers."
            ],
            [
              "store",
              "No",
              "boolean",
              "ignored",
              "Accepted for OpenAI-SDK compatibility but ignored — NeuroRoute manages retention per your organization's settings, not per request."
            ],
            [
              "metadata",
              "No",
              "object",
              "none",
              "Passed through and persisted alongside the usage event for later filtering."
            ],
            [
              "thinking_budget_tokens",
              "No",
              "integer",
              "none",
              "NeuroRoute extension: sets a reasoning model's native thinking-token budget directly (e.g. Anthropic's thinking.budget_tokens, Gemini's thinkingConfig.thinkingBudget)."
            ],
            [
              "conversation_id",
              "No",
              "string",
              "none",
              "NeuroRoute extension: enables server-side conversation memory across requests (retain-mode organizations only)."
            ],
            [
              "variables",
              "No",
              "object of string to string",
              "none",
              "NeuroRoute extension: used with model set to \"prompt:<slug>\" to fill a stored prompt template's placeholders. When set, messages is ignored in favor of the rendered template."
            ]
          ]
        },
        "note": "conversation_id, thinking_budget_tokens, and variables are NeuroRoute-specific extensions layered on top of the OpenAI-compatible schema; every other field name and behavior matches OpenAI's Chat Completions API."
      },
      {
        "heading": "Request headers",
        "paragraphs": [
          "These optional headers customize behavior for a single request, without changing your key's stored defaults."
        ],
        "table": {
          "columns": [
            "Header",
            "Values",
            "What it does"
          ],
          "rows": [
            [
              "X-Routing-Strategy",
              "cheapest, best-quality, task-aware, fastest, balanced, cascade, fusion, pipeline",
              "Overrides the routing strategy for this one request. Takes precedence over the API key's stored default strategy. See the Routing strategies doc for what each value does."
            ],
            [
              "X-Budget-Limit",
              "a dollar amount, e.g. \"0.01\"",
              "An advisory cost preference, not a hard cap. The router penalizes models priced above this figure in scoring, but an over-budget model can still be selected if every candidate exceeds it or quality considerations dominate. Hard spend enforcement comes from the separate budget-cap system (see Response headers below)."
            ],
            [
              "X-Compression-Engines",
              "comma-separated engine names",
              "Selects which context-compression engines to apply to this request. See the Compression doc for the full engine list and behavior."
            ],
            [
              "X-Prompt-Cache",
              "off, false, or 0 (any other value/absence leaves it on)",
              "Disables provider-side prompt caching for this request. Prompt caching is on by default; this header can only turn it off, and it has no effect for organizations whose retention mode already forces caching off."
            ],
            [
              "X-NR-Metadata",
              "a flat JSON object (no nested objects/arrays), up to 2 KB",
              "Arbitrary custom tags for this request. Persisted alongside the usage event so you can filter/group requests later (e.g. by feature or customer). Invalid or oversized JSON is silently ignored rather than rejecting the request."
            ],
            [
              "X-NR-Guardrails",
              "a JSON guardrails configuration",
              "Per-request override of input/output content-safety rules. See the Guardrails/Security doc for the full configuration format."
            ],
            [
              "X-Agent-Step",
              "inner or final",
              "Lets an agentic/tool-calling framework hint that this call is an intermediate step in a multi-step loop (\"inner\") rather than the final user-facing answer. Softly biases routing toward the cheapest viable strategy for that one request."
            ],
            [
              "X-Run-Id",
              "any caller-chosen string",
              "Groups multiple related completions (e.g. the steps of one agent run) under a single run identifier so they can share one dollar budget. Has no effect unless a run budget is also in force."
            ],
            [
              "X-Run-Budget-USD",
              "a dollar amount",
              "Sets a per-run spend cap for this run ID. Can only tighten below your API key's configured run-budget ceiling, never loosen it — a request trying to raise it above the key's ceiling is clamped down to that ceiling."
            ]
          ]
        }
      },
      {
        "heading": "Response headers",
        "paragraphs": [
          "NeuroRoute returns diagnostic headers on every completion response so you can see what actually happened without parsing the body."
        ],
        "table": {
          "columns": [
            "Header",
            "What it means"
          ],
          "rows": [
            [
              "X-Routed-Model",
              "The exact model ID that served the request."
            ],
            [
              "X-Routing-Strategy",
              "The strategy that was actually applied (after any header/key-default resolution)."
            ],
            [
              "X-Request-Cost",
              "The real provider cost of this request, in USD."
            ],
            [
              "X-Counterfactual-Cost",
              "What the baseline model (Claude Fable 5, priced at $10 per million input tokens and $50 per million output tokens) would have charged for the same token counts."
            ],
            [
              "X-Savings",
              "X-Counterfactual-Cost minus X-Request-Cost — never negative."
            ],
            [
              "X-Cache",
              "HIT or MISS for the exact-match response cache. See the Caching doc for details."
            ],
            [
              "X-Data-Retention",
              "Your organization's retention posture applied to this response. See the Security doc for details."
            ],
            [
              "X-Key-Budget-Day / X-Key-Budget-Month",
              "This API key's spend versus its daily/monthly cap, formatted spent/limit (limit is \"inf\" when the cap is unset)."
            ],
            [
              "X-Budget-Warning",
              "Set to \"true\" once a key's spend crosses roughly 80% of its cap."
            ],
            [
              "X-Budget-Tier / X-Budget-Window",
              "Present only on a blocked (402) request: which budget tier (run, key, workspace/org, or platform) and which window (day, month, or run) tripped the cap, alongside X-Budget-Limit/X-Budget-Spent for the tier that fired."
            ],
            [
              "X-Model-Fallback",
              "Present only when the originally routed model failed and a fallback model served the request instead. Format is originalModel->fallbackModel."
            ],
            [
              "X-Run-Budget",
              "Only present when X-Run-Id was set and run budgets are tracked: spend versus the run's cap, formatted spent/limit (\"inf\" if uncapped)."
            ]
          ]
        },
        "note": "A few other diagnostic headers exist for specific features (compression ratio, guardrails actions, fusion panel/winner, pipeline chain, fair-share usage) — those are documented alongside their respective features rather than repeated here."
      },
      {
        "heading": "Budget caps and the 402 response",
        "paragraphs": [
          "Separate from the advisory X-Budget-Limit request header, NeuroRoute enforces real dollar spend caps before every provider call, checked in order: per-run (only if X-Run-Id is set and your plan includes run budgets), then per-API-key (daily and monthly), then org/workspace (daily and monthly), then a platform-wide guardrail.",
          "The first cap that is exceeded blocks the request with HTTP 402, and the response carries X-Budget-Tier and X-Budget-Window identifying exactly which cap fired, plus X-Budget-Limit and X-Budget-Spent for that tier. A cap set to null or a non-positive number means unlimited for that tier.",
          "This is separate from the X-Budget-Limit request header described above, which only nudges routing toward cheaper models and never blocks a request on its own."
        ]
      }
    ]
  },
  {
    "id": "routing",
    "title": "Routing strategies",
    "icon": "route",
    "summary": "NeuroRoute can pick the best model for each request automatically, or you can point a request at a specific model directly. This explains the eight routing strategies, how task classification works, and how to configure a strategy for a key, a request, or an org.",
    "sections": [
      {
        "heading": "auto versus a specific model",
        "paragraphs": [
          "Set \"model\": \"auto\" (or leave it as your default) to let NeuroRoute's router classify the request and score candidate models under whichever strategy applies. This is where all the cost/quality/latency intelligence described below happens.",
          "Setting \"model\" to a specific model ID instead (for example \"claude-sonnet-4-6\") bypasses routing entirely — NeuroRoute resolves that exact model and calls it directly, with no scoring, cascade, fusion, or pipeline logic involved. Use this when you need a guaranteed specific model for a given call."
        ]
      },
      {
        "heading": "The eight strategies",
        "paragraphs": [
          "Every strategy scores candidate models on a blend of quality, cost, latency, and conversation continuity, weighted differently per strategy. The table below describes what each one actually optimizes for."
        ],
        "table": {
          "columns": [
            "Strategy",
            "Optimizes for",
            "Behavior"
          ],
          "rows": [
            [
              "cheapest",
              "Cost, heavily",
              "Cost-dominant, with an additional bias toward free/near-zero-cost models once they clear a quality floor. Runs as a cascade: tries the cheapest eligible model first and escalates to progressively stronger models (up to 3 steps) only on a hard failure — a provider error or a structurally invalid response (e.g. empty content). A low-confidence but otherwise valid response is still accepted, never escalated past."
            ],
            [
              "best-quality",
              "Quality, almost exclusively",
              "Picks the single highest-quality model for the task with cost given essentially no weight. A direct pick, not a cascade."
            ],
            [
              "task-aware",
              "Quality first, cost as a differentiator on easy tasks",
              "Quality-dominant by default, matching each request to a model strong enough for the classified task. For task types the classifier is genuinely confident are easy (general questions, conversation, summarization, factual Q&A) it shifts to a more cost-sensitive weighting so trivial requests land on cheaper models; harder task types (code, math, data analysis, creative writing, translation, multimodal) keep the quality-dominant weighting throughout."
            ],
            [
              "fastest",
              "Latency, heavily",
              "Weights expected response latency above everything else. A direct pick, not a cascade."
            ],
            [
              "balanced",
              "An even mix of quality, cost, latency, and continuity",
              "The platform default strategy. A genuine middle-ground single pick — deliberately not a cascade, so it lands on a real mid-tier model rather than collapsing to the cheapest option."
            ],
            [
              "cascade",
              "Cost, most aggressively",
              "The most cost-dominant strategy, and the most willing to escalate: starts at the cheapest eligible model and steps up through up to 4 tiers on a hard failure — a provider error or a structurally invalid response. A low-confidence but valid response is still accepted at that step; confidence is logged for model-quality feedback, not used to trigger escalation."
            ],
            [
              "fusion",
              "Quality, via a panel of models",
              "Fans the same prompt out in parallel to the top-3 highest-scored candidate models, then a cheap judge model picks the single best answer and returns it verbatim (no merging or synthesis). Every panel leg and the judge are real, billed calls."
            ],
            [
              "pipeline",
              "Quality, via sequential chaining",
              "Chains models sequentially: each model's output feeds into the next model's input as one multi-hop request."
            ]
          ]
        },
        "note": "fusion requires the Premium plan or higher. pipeline requires the Standard plan or higher. Neither has a streaming form — a streaming request that resolves to fusion or pipeline is automatically downgraded to the best-quality strategy for that call, since both need every step's complete response before they can pick or chain further."
      },
      {
        "heading": "Task classification (used by task-aware routing)",
        "paragraphs": [
          "NeuroRoute classifies every request into one of eleven task types, with a confidence score. Only task-aware routing changes its weighting based on the classified task, and only when the classifier's confidence is reasonably high — an uncertain classification keeps the quality-dominant weighting rather than risk routing a misclassified hard question to a cheap model."
        ],
        "list": [
          "code_generation",
          "code_review",
          "creative_writing",
          "math_reasoning",
          "summarization",
          "translation",
          "factual_qa",
          "data_analysis",
          "conversation",
          "multimodal",
          "general"
        ]
      },
      {
        "heading": "Setting a strategy",
        "paragraphs": [
          "There are three ways to control which strategy is used, from broadest to narrowest scope.",
          "1. Key default: set routing_strategy when creating or updating an API key (an empty value inherits your organization's default strategy, which is task-aware unless your organization has set its own default).",
          "2. Per-request override: send the X-Routing-Strategy header on any request. This takes precedence over the key's stored default for that one call. Valid values are cheapest, best-quality, task-aware, fastest, balanced, cascade, fusion, and pipeline.",
          "3. Model priority chain: an org_admin (or SISL admin) can configure an ordered list of up to 10 model IDs for your organization. When set, routing is restricted to just those models, tried in that declared order, regardless of which strategy is selected — the strategy still applies within that restricted set, but the candidate pool itself is fixed. This requires the Standard plan or higher.",
          "4. Custom routing rules: pin one specific task type (e.g. code generation) to a specific model or cascade chain from the Routing Rules page, overriding the platform default for just that task while every other task keeps using strategy scoring normally. See Other settings → Custom routing rules and config-as-code routing for the full walkthrough, including the versioned JSON alternative for more complex policies (model pins plus weighted A/B splits)."
        ]
      }
    ]
  },
  {
    "id": "evals",
    "title": "Evaluation framework",
    "icon": "checkcircle",
    "summary": "A benchmarking harness for testing models, routing strategies, and routing-config versions against your own prompt suites, scoring each response automatically and reporting quality, cost, and latency per case.",
    "sections": [
      {
        "heading": "What it does",
        "paragraphs": [
          "The evaluation framework lets you define a suite of test prompts, then run that suite against several candidates at once - a specific model, a routing strategy, or a saved routing-config version - so you can compare quality, cost, and latency side by side before committing to a change.",
          "This is a real benchmarking tool, not a simulation: every case in a run is a genuine chat completion that goes through the same routing, budget, and billing path as your production traffic. Eval calls appear in your usage and are billed normally.",
          "Runs are asynchronous. Creating a run returns immediately with a run ID; you poll for status and results as the run executes in the background.",
          "This feature is available on the Premium and Enterprise plans."
        ]
      },
      {
        "heading": "Suites and cases",
        "paragraphs": [
          "A suite is a named collection of test cases you author once and reuse across many runs. Each case is a prompt plus a scorer that grades whatever response a candidate produces for it.",
          "Suites are org-scoped configuration, not conversation content, so they are stored the same way regardless of your organization's data-retention mode."
        ],
        "table": {
          "columns": [
            "Field",
            "Required",
            "Type",
            "Description"
          ],
          "rows": [
            [
              "name",
              "Yes",
              "string",
              "Suite name. Must be non-empty and at most 200 characters. A suite name must be unique within your org."
            ],
            [
              "description",
              "No",
              "string",
              "Free-text description of what the suite tests."
            ],
            [
              "cases",
              "Yes",
              "array",
              "1 to 500 case objects."
            ],
            [
              "cases[].prompt",
              "Yes",
              "string",
              "The user prompt sent to the candidate for this case."
            ],
            [
              "cases[].system_prompt",
              "No",
              "string",
              "An optional system message prepended ahead of the prompt."
            ],
            [
              "cases[].scorer",
              "Yes",
              "string",
              "One of: exact, regex, json-schema, llm-judge."
            ],
            [
              "cases[].scorer_config",
              "Depends on scorer",
              "object",
              "Scorer-specific configuration; see the Scorers section below."
            ]
          ]
        },
        "code": {
          "lang": "json",
          "text": "POST /v1/customer/evals/suites\n{\n  \"name\": \"support-tone-checks\",\n  \"description\": \"Checks that support replies stay on-brand and structured\",\n  \"cases\": [\n    {\n      \"prompt\": \"A customer says their invoice looks wrong. Reply politely.\",\n      \"system_prompt\": \"You are a support agent for Acme.\",\n      \"scorer\": \"llm-judge\",\n      \"scorer_config\": {\n        \"rubric\": \"Reply is polite, apologizes if appropriate, and asks for the invoice number.\",\n        \"threshold\": 0.7\n      }\n    },\n    {\n      \"prompt\": \"Return the word OK and nothing else.\",\n      \"scorer\": \"exact\",\n      \"scorer_config\": { \"expected\": \"OK\" }\n    }\n  ]\n}"
        },
        "note": "Each case is capped at 32 KB, counting the combined length of prompt, system_prompt, and scorer_config together. A case with an empty prompt is rejected."
      },
      {
        "heading": "Suite endpoints",
        "table": {
          "columns": [
            "Method",
            "Path",
            "Access",
            "Notes"
          ],
          "rows": [
            [
              "POST",
              "/v1/customer/evals/suites",
              "org_admin and above",
              "Creates a suite with its cases. Validation runs before anything is persisted; a suite with an invalid case is rejected as a whole, not created partially."
            ],
            [
              "GET",
              "/v1/customer/evals/suites",
              "org_admin, org_member, org_viewer",
              "Lists suites for your org."
            ],
            [
              "GET",
              "/v1/customer/evals/suites/{id}",
              "org_admin, org_member, org_viewer",
              "Returns the suite plus its full case list."
            ],
            [
              "DELETE",
              "/v1/customer/evals/suites/{id}",
              "org_admin and above",
              "Deletes a suite. Returns 404 for an unknown or cross-org id."
            ]
          ]
        },
        "note": "Admin equivalents exist at /v1/admin/organizations/{org_id}/evals/suites... for any org, restricted to admin/sisl_admin roles."
      },
      {
        "heading": "Candidate types",
        "paragraphs": [
          "A run compares one or more candidates against the same suite. Each candidate must set exactly one of three fields - model, strategy, or config_version. Setting more than one, or none, is rejected with the error \"candidate must set exactly one of model/strategy/config_version\".",
          "A single run's candidates array can freely mix all three types, so you can compare a hard model pin, a routing strategy, and a saved routing-config version side by side in one run - each candidate is judged independently against every case in the suite."
        ],
        "table": {
          "columns": [
            "Field",
            "Meaning",
            "Behavior"
          ],
          "rows": [
            [
              "model",
              "A specific model ID, e.g. vertex-gemini-2.5-flash",
              "Routes as a direct pin, bypassing the scorer/strategy logic entirely."
            ],
            [
              "strategy",
              "A named routing strategy, e.g. cheapest, best-quality, balanced",
              "The suite's prompts are routed normally through that strategy, exactly as a live request would be."
            ],
            [
              "config_version",
              "The ID of a saved routing-config version",
              "The request is tagged so the router's config-as-code pin logic applies that version's rules."
            ]
          ]
        },
        "code": {
          "lang": "json",
          "text": "POST /v1/customer/evals/runs\n{\n  \"suite_id\": \"11111111-2222-3333-4444-555555555555\",\n  \"candidates\": [\n    { \"strategy\": \"cheapest\" },\n    { \"model\": \"vertex-claude-opus-5\" },\n    { \"config_version\": \"config-version-uuid\" }\n  ]\n}"
        },
        "note": "A run allows between 1 and 6 candidates. A model candidate is a hard pin: it is NOT filtered by PII allowed-provider policy or your key's model allowlist/denylist the way strategy and config-version candidates are - it routes straight to the named model. Keep this in mind if you pin a model that would otherwise be excluded by your org's policy."
      },
      {
        "heading": "Scorers",
        "paragraphs": [
          "Every case names exactly one scorer, and every scorer produces a status of passed, failed, or unscored - a scorer never reports the completion itself as failed; that is a separate errored status reserved for cases where the candidate call itself failed (e.g. a provider error)."
        ],
        "table": {
          "columns": [
            "Scorer",
            "scorer_config",
            "Grading logic"
          ],
          "rows": [
            [
              "exact",
              "{ \"expected\": string, \"case_sensitive\": bool }",
              "Passes when the response equals expected after trimming and collapsing whitespace. case_sensitive (default false) controls case folding only; whitespace normalization always applies."
            ],
            [
              "regex",
              "{ \"pattern\": string }",
              "Passes when the response matches the given regular expression anywhere in the text (regexp.MatchString semantics). The pattern must compile at suite-save time or the case is rejected."
            ],
            [
              "json-schema",
              "{ \"required\": [string, ...] }",
              "Passes when the response is valid JSON representing an object (not an array, number, or null) and every key listed in required is present. This is a minimal shape check - it does not validate value types or a full JSON Schema document."
            ],
            [
              "llm-judge",
              "{ \"rubric\": string, \"threshold\": number (0,1] }",
              "Routes the prompt and the candidate's response to a cheap-tier model along with your rubric, asking it to reply with a single number from 0.0 to 1.0. Passes when that score is >= threshold. rubric must be non-empty and threshold must be greater than 0 and at most 1, or the case is rejected at suite-save time."
            ]
          ]
        },
        "note": "llm-judge fails open: if the judge sub-request errors or its reply cannot be parsed as a number, the case is scored unscored rather than failed or errored - a broken judge call never counts against a candidate. The judge prompt structurally separates your rubric (system message) from the candidate's own prompt and response (wrapped in tags in a separate user message), so a candidate's response text cannot talk the judge into ignoring the rubric."
      },
      {
        "heading": "Running an evaluation",
        "paragraphs": [
          "Creating a run is asynchronous: the create call returns HTTP 202 with a run_id and status \"pending\" immediately - it does not wait for the run to finish.",
          "Only one run can be active per organization at a time. Submitting a new run while one is pending or running returns HTTP 409 with the error \"an eval run is already active for this organization\".",
          "Poll GET /v1/customer/evals/runs/{id} for status and, once cases have executed, per-case results. A run's status is one of: pending, running, complete, canceled, or failed. failed is reserved for a run that never finalized cleanly (for example, interrupted by a server restart) rather than for a normal graded failure - individual case grades are recorded as passed/failed/unscored/errored inside the results array regardless of the run's own status."
        ],
        "code": {
          "lang": "json",
          "text": "POST /v1/customer/evals/runs -> 202\n{ \"run_id\": \"...\", \"status\": \"pending\" }\n\nGET /v1/customer/evals/runs/{id} -> 200\n{\n  \"run\": {\n    \"id\": \"...\",\n    \"suite_id\": \"...\",\n    \"status\": \"complete\",\n    \"candidates\": [ { \"strategy\": \"cheapest\" }, { \"model\": \"vertex-claude-opus-5\" } ],\n    \"aggregates\": { \"0\": { \"candidate_idx\": 0, \"passed\": 8, \"failed\": 2, \"avg_score\": 0.81, \"total_cost_usd\": 0.004, \"p50_latency_ms\": 620, \"p95_latency_ms\": 1400 } },\n    \"created_by\": \"...\",\n    \"created_at\": \"...\",\n    \"started_at\": \"...\",\n    \"finished_at\": \"...\"\n  },\n  \"results\": [\n    {\n      \"candidate_idx\": 0,\n      \"case_id\": \"...\",\n      \"status\": \"passed\",\n      \"score\": 1.0,\n      \"cost_usd\": 0.0002,\n      \"latency_ms\": 540,\n      \"input_tokens\": 40,\n      \"output_tokens\": 12,\n      \"selected_model\": \"deepinfra/...\",\n      \"response\": \"OK\"\n    }\n  ]\n}"
        },
        "note": "The response field on a case result is populated only when the underlying text is available to read back - see Data retention below for when that is and isn't the case."
      },
      {
        "heading": "Canceling a run",
        "paragraphs": [
          "POST /v1/customer/evals/runs/{id}/cancel does more than flip a status flag. It updates the run's stored status to canceled and also signals the background job actually executing that run to stop: no further cases are dispatched, and the run's context is canceled so an in-flight provider call that respects the request context returns promptly rather than continuing to completion.",
          "A case that was already mid-flight when cancellation lands still finishes and its result is still recorded - cancellation stops new dispatch, it does not abandon work already in progress. The run is then finalized with status canceled rather than complete.",
          "Canceling a run that has already reached a terminal state (complete, canceled, or failed) or that does not exist returns HTTP 404."
        ]
      },
      {
        "heading": "Billing and budgets",
        "paragraphs": [
          "Eval completions are real, billed requests. Every case-candidate execution runs through the same completion pipeline as a normal chat request, including your organization's budget caps, billing metering, and PII/allowlist policy (except for a hard model-pin candidate, which - like any direct model pin - bypasses policy filtering as noted above).",
          "The one difference from a normal request is that eval traffic runs in an eval mode that skips the response caches (L1 and semantic) and conversation storage, so eval runs never pollute your production cache or get treated as conversational history.",
          "Every usage event generated by an eval run - including the llm-judge scoring sub-calls - is tagged with the run's ID, so eval spend is traceable back to the specific run that caused it in your usage data."
        ]
      },
      {
        "heading": "Data retention",
        "paragraphs": [
          "Suite and case definitions - your prompts and rubrics - are stored for every organization regardless of data-retention mode, the same as other standing configuration.",
          "Response text is a different matter. Whether a case result's response body is stored depends on your organization's retention mode at the time the run was created: retain-mode organizations get the response text stored (encrypted, using the same per-org encryption as conversation content); zero and zero-strict organizations get scores, cost, and latency for every case with no response text stored at all.",
          "When response text is stored, it is only ever stored encrypted or not at all - there is no plaintext fallback if encryption fails; that case's response is simply omitted rather than saved in the clear."
        ]
      },
      {
        "heading": "Exporting results",
        "paragraphs": [
          "GET /v1/customer/evals/runs/{id}/export downloads that run's case results as an attachment. It returns JSON by default, or CSV when called with ?format=csv.",
          "Both formats carry the same fields: candidate_idx, case_id, status, score, customer_cost_usd, latency_ms, and selected_model.",
          "Response text is intentionally left out of both export formats - exports are for comparing cost and quality across candidates, not for retrieving the underlying model output. Use the run-status endpoint if you need to read a specific response body (subject to the retention rule above)."
        ]
      },
      {
        "heading": "Access and roles",
        "paragraphs": [
          "Read access (listing/viewing suites and runs, and exporting results) is available to org_admin, org_member, and org_viewer roles.",
          "Mutating actions - creating or deleting a suite, starting a run, and canceling a run - require org_admin or above.",
          "org_billing does not have access to any eval endpoint."
        ]
      },
      {
        "heading": "Limits summary",
        "list": [
          "Suite name: required, at most 200 characters, unique per organization",
          "Cases per suite: 1 to 500",
          "Size per case: at most 32 KB (prompt + system_prompt + scorer_config combined)",
          "Candidates per run: 1 to 6",
          "Active runs per organization: 1 at a time (a second submission returns 409 while one is pending or running)"
        ]
      }
    ]
  },
  {
    "id": "caching",
    "title": "Caching",
    "icon": "layers",
    "summary": "NeuroRoute has three distinct caching layers that can each reduce cost and latency: an exact-match response cache, a semantic (near-duplicate) response cache, and provider-side prompt caching. This page explains each one, how they are scoped, and which response headers tell you what happened on a given request.",
    "sections": [
      {
        "heading": "Overview: three layers, not one",
        "paragraphs": [
          "Customers sometimes assume there is a single cache, but NeuroRoute actually runs three independent mechanisms that can each fire on the same request: an exact-match Redis cache (L1), a semantic pgvector cache (L2), and provider-side prompt caching (Anthropic and Vertex-Claude only). Only one response-body cache can serve a given request (L1 is checked first, then L2), while provider-side prompt caching is a separate, complementary mechanism that reduces the cost of calls that are NOT served from cache.",
          "All three respect the org's data retention mode. If an organization is set to zero or zero-strict retention, both the L1 and L2 response caches are bypassed entirely for both reads and writes. Provider-side prompt caching is only forced off under zero-strict; it is still allowed under plain zero retention."
        ]
      },
      {
        "heading": "Layer 1: exact-match cache (L1)",
        "paragraphs": [
          "The L1 cache is a Redis-backed exact-match cache keyed by a SHA-256 fingerprint of: the organization ID, the requested model (the string the client asked for, e.g. \"auto\" - not the model routing eventually resolves to), every message's role and text content, the temperature (if set), and a quality band derived from the routing strategy.",
          "The quality band exists specifically to stop a cheap request from being served an answer generated for a premium request. Every auto-routed request shares the literal requested model string \"auto\", so without banding a best-quality request could be served a response a cheapest request generated on a bargain-tier model - the customer would be billed for premium routing but silently receive budget-tier output. There are three bands: a cost band (cheapest, cascade, and fastest strategies - fastest is included because it optimizes for latency, which a cache hit satisfies perfectly, and sets no quality floor a cost-tier answer would violate), a balanced band (kept separate because balanced is the default strategy - grouping it with cost would make the bad case the common case), and a quality band (best-quality, task-aware, fusion, and pipeline). An unknown or empty strategy string falls into the quality band as a fail-safe, since that is the most restrictive place to put a request.",
          "Only single-turn, non-streaming, non-tool-calling requests are eligible for the L1 cache. Requests with more than one user/assistant exchange, any tool definitions or tool calls, or streaming enabled are never cached.",
          "A historical gap - where a model:\"auto\" request could never register an L1 cache HIT because the pre-routing cache read and the post-routing cache write disagreed on the key (read used the literal \"auto\", write used the resolved model) - has been fixed. The gateway now preserves the client's originally requested model in a separate field before routing mutates it, and both the read and the write key on that preserved value, so auto-routed requests do hit correctly."
        ],
        "list": [
          "Response header: X-Cache is set to MISS, HIT (L1 exact match), or SEMANTIC-HIT (L2 semantic match) - it is the same header for both cache layers, distinguished by value.",
          "On any hit, X-Cached-Model and X-Cached-Provider report which model/provider actually generated the cached response.",
          "TTL is configurable via the CACHE_TTL_SECONDS environment variable, defaulting to 86400 seconds (24 hours).",
          "L1 is gated behind the platform's premium-tier cache entitlement (an internal tier-portability mechanism); it participates only when that feature is enabled for the org."
        ],
        "note": "L1 and L2 both fail open on Redis/DB errors - a cache lookup problem degrades to a normal (uncached) request, it never blocks or errors the call."
      },
      {
        "heading": "Layer 2: semantic (L2) cache",
        "paragraphs": [
          "The semantic cache finds and reuses answers to near-duplicate prompts, not just byte-identical ones. It is backed by pgvector on the same Postgres database, and embeds prompts using Vertex AI's text-embedding-004 model.",
          "Scoping mirrors L1 closely but is not identical: only the prompt text (system + user message content) is matched by similarity. Everything else - organization, the requested model string, the quality band, and temperature - must match exactly, using the same banding logic as L1 to prevent a cost-tier answer being served to a quality-tier request. An unset temperature and an explicit temperature of 0 are treated as distinct buckets so a creative (high-temperature) request is never served the deterministic answer cached at temperature 0.",
          "The similarity threshold is configurable via SEMANTIC_CACHE_THRESHOLD, defaulting to 0.95 (a very high bar - the prompts have to be almost identical in meaning, not merely topically related).",
          "This is a per-organization opt-in toggle (organizations.semantic_cache_enabled in the database). Based on the actual endpoint wiring, this is set through the SISL-admin-only organization update endpoint (PATCH /v1/admin/organizations/{org_id}) and the equivalent admin-only MCP tool - there is no customer-facing self-service endpoint to flip this toggle. A customer who wants semantic caching enabled needs to request it from SISL."
        ],
        "list": [
          "Response header: X-Cache: SEMANTIC-HIT, plus X-Cache-Similarity (the match score) and X-Cached-Model / X-Cached-Provider.",
          "Same eligibility rules as L1: no streaming, no tools, single-turn only.",
          "Same retention gating as L1: zero/zero-strict orgs never read or write the semantic cache."
        ],
        "note": "Only the prompt is fuzzy-matched. Org, model, quality band, and temperature must all match exactly - a semantic hit never crosses those boundaries."
      },
      {
        "heading": "Layer 3: provider-side prompt caching",
        "paragraphs": [
          "This is a separate mechanism from L1/L2: instead of caching the whole response, it asks the underlying provider (Anthropic direct, or Anthropic models served via Vertex) to cache the system-prompt prefix of the request itself, so repeated calls that reuse the same system prompt (e.g. successive turns of a conversation, or the same system prompt across many customers) are billed and processed faster on the provider's side - independent of whether NeuroRoute's own response cache has anything to do with it.",
          "It is default ON for every request. Customers can disable it per-request with the X-Prompt-Cache header (values off, false, or 0 all disable it).",
          "Under zero-strict retention, provider-side prompt caching is forced off entirely and the X-Prompt-Cache header cannot re-enable it - the reasoning is that the provider itself would be retaining the system prefix for its own cache TTL, which conflicts with a zero-strict retention promise. Plain zero retention (as opposed to zero-strict) still allows it.",
          "Cost mechanics, per the adapter's own code comments: a cache WRITE (the first call that establishes the cached prefix) costs about 25% more than a normal input token, while a subsequent cache READ (a later call that hits the same cached prefix, within roughly a 5-minute provider-side TTL) costs about 90% less than a normal input token. Below the model's minimum cacheable prefix size, the cache marker is simply ignored - no error, no extra cost."
        ]
      },
      {
        "heading": "Prompt-cache warming (opt-in)",
        "paragraphs": [
          "An additional, separate feature: after a routed (non-streaming, non-cascade, non-fusion) request completes, the gateway can asynchronously send a minimal warm-up call to the runner-up model (the next-best candidate from that routing decision) using the same system prefix, so that if a later request in the same conversation switches models - due to session stickiness breaking, a failover, or a task-type change - it lands on an already-warm provider cache instead of paying full price for the first call on that model.",
          "This is off by default, controlled by the CACHE_WARMING_ENABLED environment variable. It only fires when the system prefix is at least 2048 characters (below that, caching is a no-op anyway) and prompt caching is enabled for the request. Warm attempts are deduplicated per (model, prefix) for about 4 minutes and are entirely fire-and-forget - a failure never affects the real request.",
          "Based on the current call site in the gateway, warming is wired only into the plain non-streaming completion path. It is not invoked from the cascade, fusion, or streaming execution paths."
        ],
        "list": [
          "Response header: X-Cache-Warm: scheduled, present only when a warm was actually kicked off."
        ]
      },
      {
        "heading": "Quick reference",
        "table": {
          "columns": [
            "Layer",
            "Match type",
            "Scope/key",
            "Header(s)",
            "Config"
          ],
          "rows": [
            [
              "L1 exact cache",
              "Byte-exact",
              "org + requested model + messages + temperature + quality band",
              "X-Cache: HIT/MISS, X-Cached-Model, X-Cached-Provider",
              "CACHE_TTL_SECONDS (default 86400s)"
            ],
            [
              "L2 semantic cache",
              "Similarity ≥ threshold on prompt text",
              "org + requested model + quality band + temperature (exact); prompt (similarity)",
              "X-Cache: SEMANTIC-HIT, X-Cache-Similarity, X-Cached-Model, X-Cached-Provider",
              "SEMANTIC_CACHE_THRESHOLD (default 0.95); per-org opt-in, admin-set"
            ],
            [
              "Provider prompt cache",
              "Provider-managed prefix cache",
              "Anthropic / Vertex-Claude system prefix",
              "X-Prompt-Cache request header (off/false/0 to disable)",
              "Default on; forced off under zero-strict retention"
            ],
            [
              "Prompt-cache warming",
              "N/A (pre-warm, not a lookup)",
              "Runner-up model + system prefix",
              "X-Cache-Warm: scheduled",
              "CACHE_WARMING_ENABLED (default off)"
            ]
          ]
        }
      },
      {
        "heading": "Retention interaction",
        "paragraphs": [
          "Every layer respects the org's retention mode. Organizations set to zero or zero-strict retention never read or write the L1 or L2 response caches - a cache lookup for those orgs always behaves as a MISS with no write afterward. Zero-strict additionally forces off provider-side prompt caching. Plain retain-mode organizations (the default, with a configurable content-retention window) participate in all three layers normally."
        ]
      }
    ]
  },
  {
    "id": "compression",
    "title": "Context compression",
    "icon": "filter",
    "summary": "NeuroRoute can shrink a request's message content before it is sent to a model provider, using a composable pipeline of named engines. This page lists every engine currently registered, what each one actually does, how the engine list is resolved for a given request, and the response headers that report what happened.",
    "sections": [
      {
        "heading": "How it fits together",
        "paragraphs": [
          "Compression is org-gated: it only runs at all when the compression subsystem is enabled and the organization is entitled to the feature. Within that, the actual list of engines applied to a given request is resolved in a fixed order: an explicit X-Compression-Engines request header takes precedence over a per-API-key default engine list, which takes precedence over the organization's default engine list, which takes precedence over running nothing.",
          "The per-API-key list lives on the api_keys.compression_engines field and can be set at key creation or via the key update endpoint. The org-level default only applies when the organization has compression turned on; when it does, and no header or per-key override is present, the org gets a fixed default engine set rather than every registered engine.",
          "Unknown engine names in a header or key config are silently skipped rather than causing an error - a stale or misspelled engine name never breaks a request."
        ]
      },
      {
        "heading": "The current engine registry",
        "paragraphs": [
          "As of this writing, seven engines are registered: normalize, command-output, rag-dedup, llmlingua2, tool-result-filter, caveman, and tabular."
        ],
        "table": {
          "columns": [
            "Engine",
            "In default set?",
            "What it actually does"
          ],
          "rows": [
            [
              "normalize",
              "Yes",
              "Deterministic, near-lossless whitespace normalization and JSON minification, plus exact-duplicate text removal. Applies to any text content block in any message."
            ],
            [
              "command-output",
              "Yes",
              "Detects blocks that look like command/log output (timestamps, log levels, file:line references, diff markers, shell prompts, stack-trace frames - at least half the lines in the block must match) and, only for blocks longer than 60 lines, truncates them to the first 20 and last 20 lines with an explicit '... N lines omitted ...' marker. Ordinary prose is left untouched."
            ],
            [
              "rag-dedup",
              "Yes",
              "Drops text blocks that are byte-identical (after whitespace normalization) to an earlier block in the same request, keeping the first occurrence. Useful when the same retrieved chunk shows up again in a later turn. Only exact/verbatim repeats are removed - it is not a fuzzy/semantic dedup - and blocks shorter than 40 characters are never touched."
            ],
            [
              "llmlingua2",
              "No (opt-in only)",
              "Calls a separate ML sidecar service (Microsoft's LLMLingua-2 model) to lossily compress text. Excluded from the org default set because it is lossy and adds both latency and cost; it's available via an explicit header (the Playground UI enables it by default) or per-key configuration."
            ],
            [
              "tool-result-filter",
              "No",
              "Truncates oversized tool-call result content (both the OpenAI-style tool-role message content and Anthropic-style tool_result content blocks) to a default cap of 8 KB, appending a notice stating how many bytes were cut. Aimed at agent loops (LangChain/CrewAI/OpenAI Agents style) where a single tool result - a web search or file read - can otherwise blow up the replayed context on every turn."
            ],
            [
              "caveman",
              "No",
              "A rule-based prose-tightening engine: it runs a fixed list of regex substitutions that strip verbose filler phrasing (e.g. 'in order to' to 'to', 'it is important to note that' to 'Note:', 'utilize' to 'use', 'basically'/'actually'/'obviously' removed outright) from user and system message text only - it never touches assistant or tool messages, and only applies to text blocks of at least 300 characters, to avoid mangling short messages or code snippets."
            ],
            [
              "tabular",
              "No",
              "Detects large CSV/TSV or markdown-table-shaped blocks (at least 40 lines that look tabular by delimiter density) in user/system messages, deduplicates consecutive identical rows, and if there are still more than 20 data rows, keeps the header plus the first 20 rows and appends a '[... N row(s) omitted by tabular compaction ...]' notice. It never parses or transforms cell values, only drops rows."
            ]
          ]
        },
        "note": "The default engine set applied when an org has compression on but no header/key override is present is exactly: normalize, command-output, rag-dedup. llmlingua2, tool-result-filter, caveman, and tabular are opt-in only (via header or per-key config), and this list can grow over time - always check the current default set rather than assuming it matches an older document."
      },
      {
        "heading": "Response headers",
        "list": [
          "X-Compression-Ratio: a value from 0 to 1 representing the fraction of tokens removed (0 = no reduction). Only set when compression actually saved tokens.",
          "X-Compression-Tokens: the token count before and after compression, formatted as \"<raw>-><compressed>\" (e.g. \"1500->900\").",
          "X-Compression-Engines-Applied: a comma-separated list of engine names that actually reduced the token count on this request - not simply the list that was requested. An engine that ran but produced no change (for example, command-output on a block that didn't look like log output) is left off this list even though it was in the resolved pipeline."
        ],
        "note": "All three headers are only set when at least one engine in the pipeline actually reduced tokens; a request where compression ran but changed nothing emits none of them."
      },
      {
        "heading": "The LLMLingua-2 sidecar",
        "paragraphs": [
          "The llmlingua2 engine does not run ML inference inline - it calls out to a separate Cloud Run service running Microsoft's LLMLingua-2 model. The engine is completely inert (a pass-through no-op) until the LLMLINGUA_SIDECAR_URL environment variable is configured to point at that service.",
          "The engine fails open on every kind of error - authentication failure, HTTP error, malformed response, or a response that came back the same size or larger than the input (an inflation guard) - by simply returning the original, uncompressed text. A sidecar outage or timeout never blocks or fails the customer's request; it just means that request's llmlingua2 pass was a no-op.",
          "The sidecar call has its own timeout, configurable via LLMLINGUA_TIMEOUT_MS (defaulting to 5000ms / 5 seconds), separate from the overall request timeout, specifically so very large contexts on CPU inference can be given more headroom without changing anything else about the request path."
        ]
      },
      {
        "heading": "Example: overriding engines on a single request",
        "code": {
          "lang": "bash",
          "text": "curl https://neuroroute.sislcloudworx.ai/v1/chat/completions \\\n  -H \"Authorization: Bearer nr_your_api_key\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"X-Compression-Engines: normalize,rag-dedup,tool-result-filter\" \\\n  -d '{\n    \"model\": \"auto\",\n    \"messages\": [{\"role\": \"user\", \"content\": \"...\"}]\n  }'"
        },
        "paragraphs": [
          "The header value is a comma-separated, ordered list of engine names. Engines run in the order listed; an unrecognized name is skipped rather than rejected."
        ]
      },
      {
        "heading": "Example: setting a default at the key or org level",
        "paragraphs": [
          "To set a default engine list on a specific API key, include compression_engines as an ordered array of engine names when creating the key, or update it later via the key-update endpoint (PATCH /v1/auth/keys/{id}) with the same field.",
          "The org-wide default set (normalize, command-output, rag-dedup) is a SISL-admin-set toggle, not something your own org_admin can flip themselves - contact SISL to have compression turned on org-wide. Once enabled, any request from that org with no header and no per-key override automatically gets that default set applied. Setting compression_engines on your own keys, shown above, is fully self-service regardless of the org-wide toggle."
        ],
        "code": {
          "lang": "bash",
          "text": "curl -X PATCH https://neuroroute.sislcloudworx.ai/v1/auth/keys/YOUR_KEY_ID \\\n  -H \"Authorization: Bearer nr_your_api_key\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"compression_engines\": [\"normalize\", \"rag-dedup\", \"tabular\"]\n  }'"
        }
      }
    ]
  },
  {
    "id": "security",
    "title": "Security & privacy",
    "icon": "shield",
    "summary": "How NeuroRoute protects your data: data retention modes, encryption at rest, right-to-be-forgotten erasure, GDPR export, content-safety guardrails, and what we log. Every setting below states plainly whether your team can change it yourselves or whether it is set by SISL on your behalf.",
    "sections": [
      {
        "heading": "Data retention modes",
        "paragraphs": [
          "Every organization runs under one of three data-retention modes, which controls whether NeuroRoute stores your conversation content at all.",
          "zero-strict stores nothing: no conversation content is saved, the response cache (both the exact-match cache and the semantic cache) is never read or written for your traffic, and provider-side prompt caching is force-disabled even if you ask for it. This is the strictest mode.",
          "zero also stores no conversation content and does not read or write either cache layer, but it does allow provider-side prompt caching (e.g. Anthropic's cache_control), since that cache lives on the provider's side, not ours.",
          "retain stores conversation history and allows both cache layers, and content is automatically purged after a configurable window (1 to 365 days, default 30) by a daily background sweep.",
          "Every chat completion response carries an X-Data-Retention header showing the exact resolved mode: zero-strict, zero, or retain:<N>d (for example retain:30d), so you always have request-level proof of which policy applied.",
          "If your organization is on zero-strict or zero and you send a request with a conversation_id, the request is rejected with a 400 error rather than silently skipping storage — we never guess.",
          "For organizations on retain mode, a separate request content logging toggle controls whether stored conversation content is fully readable in Request Logs (encrypted at rest under your org's key, decrypted for display) or metadata only, which is the default. Switching this to content mode requires retention_mode to be retain — zero and zero-strict organizations store nothing, so there is nothing to log in full regardless of this setting."
        ],
        "table": {
          "columns": [
            "Setting",
            "Endpoint",
            "Who can change it",
            "What it does"
          ],
          "rows": [
            [
              "Retention mode (zero-strict / zero / retain)",
              "GET / PUT /v1/customer/data-retention",
              "org_admin (your own org) or SISL admin",
              "Controls whether conversation content is stored at all, and whether caching is allowed"
            ],
            [
              "Content retention window (1-365 days)",
              "PUT /v1/customer/data-retention",
              "org_admin (your own org) or SISL admin",
              "How long stored conversation content is kept before the daily sweep purges it (retain mode only)"
            ],
            [
              "Request content logging (metadata / content)",
              "PUT /v1/customer/data-retention",
              "org_admin (your own org) or SISL admin",
              "For retain-mode orgs, whether stored content is fully readable in Request Logs or metadata only (default)"
            ]
          ]
        },
        "note": "Data retention is self-service: any org_admin can view and change it from Settings → Data retention, or directly via GET/PUT /v1/customer/data-retention — no need to contact SISL. SISL admin can still set or override it on your behalf (e.g. during onboarding). Every change, by either path, is recorded in the audit trail with the before-and-after value, not just the new setting."
      },
      {
        "heading": "Encryption at rest",
        "paragraphs": [
          "For organizations on retain mode, conversation content is encrypted at rest using AES-256-GCM. Each organization has its own data encryption key (DEK), and that DEK is itself wrapped by a platform key-encryption key (KEK) before it's stored -- your content is never protected by a single shared key.",
          "By default the platform KEK is derived from NeuroRoute's master encryption key. On deployments where Google Cloud KMS is configured as the primary KEK, DEKs are wrapped by a real KMS key instead, with the platform's own master-key KEK kept registered only to decrypt older rows -- a zero-downtime way to roll the platform's own key material forward.",
          "Encrypted sinks today are conversation message content and the exact-match (L1) response cache. The semantic cache is deliberately excluded, since it needs to remain searchable, and semantic caching is off by default and opt-in per organization.",
          "Crypto-shred: deactivating an organization's DEK version (which happens automatically during org erasure, see below) makes every piece of content that was ever encrypted under that DEK version permanently unreadable -- including in backups -- because the wrapped key material needed to unwrap it is gone. This is why crypto-shredding is treated as equivalent to secure deletion.",
          "DEKs are also rotated periodically and re-encrypt existing content onto the new version; older DEK versions are only deactivated once zero content remains encrypted under them, so a rotation in progress can never make data momentarily unreadable."
        ],
        "table": {
          "columns": [
            "Setting",
            "Endpoint",
            "Who can change it",
            "What it does"
          ],
          "rows": [
            [
              "Customer-managed encryption key (CMEK)",
              "GET / PUT /v1/customer/cmek",
              "org_admin (your own org, Enterprise plan) or SISL admin",
              "Points your organization's DEK wrapping at a KMS key in your own GCP project instead of NeuroRoute's platform key"
            ],
            [
              "DEK rotation schedule",
              "Admin-triggered only",
              "SISL-configured (platform-wide env setting)",
              "How often each organization's data encryption key is rotated and content re-encrypted onto the new version"
            ]
          ]
        },
        "note": "CMEK is genuinely a customer-side kill switch: once your organization's key wraps the DEK, revoking NeuroRoute's IAM permission on that KMS key immediately and permanently prevents NeuroRoute from decrypting any of your stored content, past or future, without you deleting anything on our side. Setting the key name is self-service for Enterprise-plan organizations — any org_admin can set it from Settings → Customer-managed encryption key (CMEK), or directly via PUT /v1/customer/cmek, once you've granted the appropriate IAM role to our service account on your KMS key. Clearing it back to the platform key (an empty value) is always allowed on any plan. Every change is recorded in the audit trail with the before-and-after key name."
      },
      {
        "heading": "Organization erasure (right to be forgotten)",
        "paragraphs": [
          "Your organization admin can request full erasure of your organization's data at any time. This is genuinely self-service: any user with the org_admin role can submit, check, and cancel an erasure request without contacting SISL.",
          "Requesting erasure schedules it 7 days out by default, giving you a grace period to cancel if it was a mistake. During those 7 days you can cancel the pending request and nothing is touched.",
          "When erasure executes: all conversation content and semantic-cache entries are permanently deleted, your organization's encryption key is crypto-shredded (making any remaining backup copies of your content unreadable), all API keys and customer-supplied provider keys are revoked, and user identities are anonymized (emails, names and profile pictures are scrubbed, though internal ID references are kept intact so past records stay consistent).",
          "Usage events, billing records, and value/savings metrics are deliberately retained after erasure -- these are billing and legal-basis records, not conversation content, and keeping them lets us continue to produce accurate historical invoices.",
          "A completion certificate (counts of what was deleted, by category) is attached to the erasure request once it finishes, and you can fetch its status at any time."
        ],
        "table": {
          "columns": [
            "Setting",
            "Endpoint",
            "Who can change it",
            "What it does"
          ],
          "rows": [
            [
              "Request erasure",
              "POST /v1/customer/erasure",
              "org_admin (your own org)",
              "Schedules erasure 7 days out (or immediately if requested by a SISL admin on your behalf)"
            ],
            [
              "Cancel erasure",
              "POST /v1/customer/erasure/cancel",
              "org_admin (your own org)",
              "Cancels a pending erasure request before it executes"
            ],
            [
              "Check erasure status",
              "GET /v1/customer/erasure",
              "Any org role",
              "Shows status, scheduled date, and the completion certificate once executed"
            ],
            [
              "Legal hold",
              "Admin-set only",
              "SISL admin only",
              "Blocks erasure execution (and the retention sweep) entirely while active"
            ]
          ]
        },
        "note": "Erasure requires the Erasure feature on your plan (Premium tier and above). Legal hold is a SISL-side control used when data must be preserved for legal or compliance reasons -- your organization cannot set or clear it yourselves, and it also blocks the routine retention sweep, not just erasure."
      },
      {
        "heading": "GDPR data export (Article 20 portability)",
        "paragraphs": [
          "Any org_admin can export a full, read-only copy of your organization's data at any time -- this is fully self-service and does not require SISL involvement.",
          "The synchronous endpoint streams the export directly as the response. Add ?format=csv to get a ZIP of CSV files (one file per data category, with conversations flattened to one row per message); omit it for the default JSON bundle.",
          "For larger exports, an asynchronous variant submits a background job, lets you poll its status, and then proxies the finished file back through the gateway once it's ready -- useful when the synchronous export would otherwise be too large or slow to hold open as one HTTP response.",
          "The export includes your organization profile, users, API key metadata (never key secrets), provider-key metadata (never the underlying key values), customer-facing billing records, conversation content (only for organizations on retain mode -- zero and zero-strict orgs have nothing to export there), and usage events.",
          "Nothing in the export ever includes NeuroRoute's own cost basis, margin, or internal pricing fields -- only the customer-facing figures you already see in your billing summary.",
          "Legal hold does not block export, since export is read-only and doesn't touch or delete anything."
        ],
        "code": {
          "lang": "bash",
          "text": "# Full JSON export, streamed directly\ncurl -H \"Authorization: Bearer nr_...\" \\\n  https://neuroroute.sislcloudworx.ai/v1/customer/export -o export.json\n\n# CSV/ZIP export\ncurl -H \"Authorization: Bearer nr_...\" \\\n  \"https://neuroroute.sislcloudworx.ai/v1/customer/export?format=csv\" -o export.zip\n\n# Async export: submit, then poll\ncurl -X POST -H \"Authorization: Bearer nr_...\" \\\n  https://neuroroute.sislcloudworx.ai/v1/customer/export/async\n# -> {\"job_id\": \"...\"}\ncurl -H \"Authorization: Bearer nr_...\" \\\n  https://neuroroute.sislcloudworx.ai/v1/customer/export/async/<job_id>"
        },
        "table": {
          "columns": [
            "Setting",
            "Endpoint",
            "Who can change it",
            "What it does"
          ],
          "rows": [
            [
              "Synchronous export",
              "GET /v1/customer/export",
              "org_admin (your own org)",
              "Streams your org's data now, as JSON (default) or a CSV ZIP (?format=csv)"
            ],
            [
              "Async export",
              "POST /v1/customer/export/async then GET .../async/{job_id}",
              "org_admin (your own org)",
              "Submits a background export job and lets you poll for and download the result"
            ]
          ]
        }
      },
      {
        "heading": "Guardrails: content-safety rules",
        "paragraphs": [
          "Guardrails are configurable rules that inspect a request before it's routed (input rules) or the model's response before it's returned (output rules). Each rule you enable has an action -- input rules support block, flag, or redact; output rules support block or flag only (there's no redaction step after generation).",
          "flag means the rule's trigger is logged and returned in response headers but the request still proceeds normally; block stops the request/response outright; redact (input rules only) masks the matched content in the message before it's sent to the model.",
          "The 8 available input rules today are: pii (detects Aadhaar, PAN, email, phone, credit card, and SSN patterns, and can redact them), denylist (blocks/flags text matching your own custom regex patterns -- you must supply the patterns; there is no built-in default list), length-cap (rejects requests whose combined user-message text exceeds a character limit, default 50,000), secret-detector (flags credential-shaped strings like AWS access keys, OpenAI-style API keys, GitHub tokens, and PEM private key blocks), webhook (posts a snippet of the content to your own HTTP endpoint and lets that endpoint decide), prompt-injection (heuristic detection of \"ignore your previous instructions\"-style phrasing), vision (caps image size and can restrict allowed image MIME types), and moderation (routes a classification prompt to a cheap model and blocks content it judges as a policy violation; you can override its system prompt).",
          "The 4 available output rules are: output-length-cap (caps response length), output-denylist (regex match against the model's response text), json-schema (validates the response against a JSON schema), and webhook (same mechanism as the input version, applied to the response instead).",
          "If guardrails are enabled for your organization but you haven't configured a custom rule set, a sensible default set applies automatically: pii (flag), secret-detector (block), length-cap (block, 50,000 chars), and prompt-injection (flag). Denylist has no default -- it only ever runs when you explicitly configure patterns."
        ],
        "table": {
          "columns": [
            "Setting",
            "Endpoint",
            "Who can change it",
            "What it does"
          ],
          "rows": [
            [
              "View guardrail configuration",
              "GET /v1/customer/guardrails",
              "Any org role",
              "Returns whether guardrails are enabled and your current rule configuration"
            ],
            [
              "Set guardrail configuration",
              "PUT /v1/customer/guardrails",
              "org_admin (your own org)",
              "Enables/disables guardrails and sets the rule list: [{name, action, params, fail_closed}, ...]"
            ]
          ]
        },
        "note": "Setting a custom guardrail configuration requires the Guardrails feature (Standard tier and above). The X-Guardrails and X-Guardrails-Action response headers report which rule (if any) fired on a given request."
      },
      {
        "heading": "PII detection and provider restriction",
        "paragraphs": [
          "Independently of the guardrails pii rule above, NeuroRoute's routing layer scans every request for personally identifiable information (currently Aadhaar numbers, PAN cards, email addresses, phone numbers, credit card numbers, and US Social Security Numbers). If any is detected, routing for that request is automatically restricted to self-hosted models only -- your prompt is never sent to a third-party provider for that request.",
          "This restriction is fail-open in one specific sense only: if the PII scan itself errors or panics, the request proceeds without the restriction rather than failing outright, so a scanner bug never blocks legitimate traffic. It is not something you configure -- it applies automatically to every request on every plan."
        ]
      },
      {
        "heading": "What we log",
        "paragraphs": [
          "Request-path logs are metadata only by default: organization, model used, input/output token counts, provider cost, and latency. Prompt text and response text are never written to these logs.",
          "The one exception is the free-text comment field on POST /v1/feedback, which is customer-authored text you type yourself. By default only its length is logged (comment_len=N); the actual comment text is only written to logs when the LOG_REQUEST_CONTENT setting is turned on, which is off by default."
        ],
        "table": {
          "columns": [
            "Setting",
            "Who can change it",
            "What it does"
          ],
          "rows": [
            [
              "LOG_REQUEST_CONTENT",
              "SISL-configured (platform-wide deployment setting)",
              "Whether the free-text feedback comment is logged in full, versus just its length"
            ]
          ]
        },
        "note": "This is a deployment-level environment variable, not a per-organization dashboard toggle -- ask SISL if you need it changed for compliance reasons."
      }
    ]
  },
  {
    "id": "settings",
    "title": "Other settings",
    "icon": "settings",
    "summary": "The self-service settings your organization admin can configure directly, beyond routing, caching, compression, evals and security -- standing context, output style, model priority, custom routing rules, team members and roles, workspaces, bring-your-own provider keys, A2A agents, SSO, webhooks, alerts, prompt templates, scheduled exports, and per-key budget caps.",
    "sections": [
      {
        "heading": "Project context and sprint context",
        "paragraphs": [
          "Project context is a standing system-preamble prepended to every request your organization sends -- a place to put project conventions, glossary terms, or tone guidance so you don't have to repeat them in every prompt. It resolves per API key first, then falls back to your organization's default.",
          "Sprint context is a second, independent layer meant for what your team is building this week -- something you expect to update more often than the longer-lived project context. Unlike project context, it is organization-level only (no per-key override).",
          "When both are set, they're concatenated into one system message: project context first, then sprint context appended after it. Keeping project context first means the stable text sits at the very front of the prompt, which is the best position for provider-side prompt caching -- editing sprint context week to week never disturbs that cached prefix.",
          "Both are configuration, not conversation content, so they are injected and apply under every data-retention mode, including zero and zero-strict -- and neither is ever stored back into conversation history."
        ],
        "table": {
          "columns": [
            "Setting",
            "Endpoint",
            "Who can change it",
            "What it does"
          ],
          "rows": [
            [
              "Project context (org default)",
              "GET/PUT /v1/customer/project-context",
              "org_admin (your own org)",
              "Standing system preamble, up to 32 KB, prepended to every request; per-key override available at key creation"
            ],
            [
              "Sprint context",
              "GET/PUT /v1/customer/sprint-context",
              "org_admin (your own org)",
              "A second, more frequently updated system-preamble layer, up to 32 KB, org-wide only, appended after project context"
            ]
          ]
        }
      },
      {
        "heading": "Concise output",
        "paragraphs": [
          "When enabled, a brevity directive (\"Be concise and direct. Avoid unnecessary filler, hedging, or repetition.\") is appended to the system message on every request, unless your own prompt already contains an explicit style hint like \"concise\", \"brief\", \"verbose\", or \"detailed\" -- in which case your own instruction is respected and nothing is added.",
          "This is available on every plan tier, including the entry-level Basic plan."
        ],
        "table": {
          "columns": [
            "Setting",
            "Endpoint",
            "Who can change it",
            "What it does"
          ],
          "rows": [
            [
              "Concise output toggle",
              "GET/PUT /v1/customer/concise-output",
              "org_admin (your own org)",
              "Turns the automatic brevity directive on or off for your organization"
            ]
          ]
        },
        "note": "A response that had the directive applied carries an X-Concise-Output: applied header."
      },
      {
        "heading": "Model priority chain",
        "paragraphs": [
          "An ordered list of up to 10 model IDs. When set, routing is restricted to just this list, tried in the order you specify -- useful if you want a hard fallback chain (\"always try model A first, then B, then C\") rather than letting the router's normal scoring decide freely."
        ],
        "table": {
          "columns": [
            "Setting",
            "Endpoint",
            "Who can change it",
            "What it does"
          ],
          "rows": [
            [
              "Model priority chain",
              "GET/PUT /v1/customer/model-priority-chain",
              "org_admin (your own org)",
              "Restricts and orders routing candidates to a specific model list (max 10 entries)"
            ]
          ]
        },
        "note": "Requires the Model Priority Chain feature (Standard tier and above)."
      },
      {
        "heading": "Custom routing rules and config-as-code routing",
        "paragraphs": [
          "The Routing Rules page (Developer section of the nav) lets you pin a specific task type (code generation, summarization, translation, etc.) to a specific model or an ordered cascade chain for your organization, overriding the platform default for that task. Deleting your rule reverts that task back to the platform default rather than to no rule at all.",
          "Precedence is strict and always in this order: your organization's rule for a task beats the platform default for that task, which beats automatic score-based routing. The page shows each task's current effective source as a badge -- \"your rule\", \"platform default\", or \"auto\" -- so you always know which one is actually in force before you touch anything.",
          "Click Override (or Edit, if you already have a rule) on any task to open its chain editor: add one or more models in the order you want them tried, use Suggest to auto-fill a chain for a given strategy (e.g. cheapest or best-quality) as a starting point, then Apply to save. \"Use platform default\" on a task with your own rule removes it and reverts to the platform default in one click.",
          "Config-as-code routing is a more powerful, versioned alternative available from Settings: you author a JSON policy describing model pins and weighted A/B splits (matched on request metadata and workspace), save it as a new version, and explicitly activate the version you want live. This gives you a full history of routing policy changes and an easy rollback path (just activate an older version again). Rules and config-as-code routing can both be in effect at once; a config-as-code match is resolved before task classification, so it takes priority over the per-task rules described above for any request it matches."
        ],
        "table": {
          "columns": [
            "Setting",
            "Endpoint",
            "Who can change it",
            "What it does"
          ],
          "rows": [
            [
              "Custom routing rules",
              "GET/PUT /v1/customer/routing-rules",
              "org_admin (your own org)",
              "Task-type to model pin/chain overrides for your organization"
            ],
            [
              "Config-as-code routing",
              "GET/PUT /v1/customer/routing-config, GET .../versions, POST .../activate",
              "org_admin (your own org)",
              "Versioned JSON routing policy with pinned models and weighted A/B splits"
            ]
          ]
        },
        "note": "Custom routing rules require the Custom Routing Rules feature (Standard tier and above) -- attempting to save one without it surfaces the real reason (\"requires the X plan or higher\"), not a generic failure. Config-as-code routing has no separate plan-tier gate of its own."
      },
      {
        "heading": "A2A agents (agent-to-agent protocol)",
        "paragraphs": [
          "The A2A Agents page (Developer section of the nav) lets you register named agents that expose a task-dispatch endpoint compatible with Google's A2A (Agent-to-Agent) protocol, so external A2A-aware orchestrators can discover and call into NeuroRoute-routed agents you define.",
          "Each agent has a unique agent_id (slug, fixed once created), a display name and description, and optionally a model_pin (route every task for this agent to one specific model), a prompt_slug (apply one of your saved prompt templates), and/or a routing_config UUID (apply one specific config-as-code routing version). Leaving all three blank routes the agent's tasks through normal automatic routing.",
          "Marking an agent Public makes it discoverable by other organizations, not just your own -- leave this off for anything internal.",
          "Registering, editing, and deleting agents is available on every plan. Actually dispatching a task to an agent (POST /a2a/agents/{agent_id}/tasks) requires the Premium plan or higher -- you can build out and review your agent registry on any plan, but task dispatch itself is gated."
        ],
        "table": {
          "columns": [
            "Setting",
            "Endpoint",
            "Who can change it",
            "What it does"
          ],
          "rows": [
            [
              "Agent registry",
              "GET/POST /a2a/agents, PUT/DELETE /a2a/agents/{agent_id}",
              "org_admin (your own org) to write; any org role to view",
              "Create, edit, and remove A2A agent definitions for your organization"
            ],
            [
              "Task dispatch",
              "POST /a2a/agents/{agent_id}/tasks",
              "Any org role, Premium plan required",
              "Dispatches a task to a registered agent per the A2A protocol"
            ]
          ]
        },
        "note": "Registering an agent never requires a plan upgrade; dispatching a task to it requires Premium. If your organization is below Premium, the page shows a banner explaining this rather than blocking agent management outright."
      },
      {
        "heading": "Team members and roles",
        "paragraphs": [
          "The Team Members page (Team section of the nav) manages who can access your organization and what they can do. Invite a teammate by email, pick their role, and optionally assign them to a workspace up front -- they receive an email invitation to accept.",
          "There are four assignable organization roles. org_admin can do everything: manage billing, provider keys, API keys, team members, and every self-service setting described throughout these docs. org_member can use the API (Playground, Routing Explorer, own API keys) and see usage analytics, but cannot see billing/costs or change org-wide settings. org_viewer can see everything read-only (including billing) but cannot make chat completion calls or change any setting. org_billing is the narrowest role -- Billing & Costs and My Provider Keys only, nothing else (no usage analytics, no API Keys page, no settings, no logs, no completions).",
          "You can change a member's role at any time from the dropdown in their row, add or remove their workspace memberships with the toggle buttons (a member can belong to more than one workspace), deactivate a member to revoke their access while keeping their history visible (and reactivate them later), and resend or cancel a pending invitation before it's accepted."
        ],
        "table": {
          "columns": [
            "Setting",
            "Who can change it",
            "What it does"
          ],
          "rows": [
            [
              "Invite team member",
              "org_admin (your own org)",
              "Sends an email invitation with a chosen role and (optional) workspace"
            ],
            [
              "Change member role",
              "org_admin (your own org)",
              "Moves a member between org_admin / org_member / org_viewer / org_billing"
            ],
            [
              "Workspace membership",
              "org_admin (your own org)",
              "Adds or removes a member from one or more workspaces"
            ],
            [
              "Deactivate / reactivate member",
              "org_admin (your own org)",
              "Revokes or restores a member's access without deleting their history"
            ],
            [
              "Resend / cancel invitation",
              "org_admin (your own org)",
              "Re-sends the invitation email, or withdraws it before it's accepted"
            ]
          ]
        },
        "note": "org_billing is deliberately narrower than org_viewer -- it exists for finance/accounts-payable contacts who need billing visibility and nothing else. See the role descriptions above before assigning it to someone who also needs day-to-day API or dashboard access."
      },
      {
        "heading": "Single sign-on (SSO)",
        "paragraphs": [
          "NeuroRoute supports both generic OIDC and SAML 2.0 single sign-on, each configured independently and both fully self-service for your org_admin -- you don't need SISL to set these up.",
          "Note that the two protocols sit at different plan tiers: OIDC and SAML SSO are both Enterprise-tier features."
        ],
        "table": {
          "columns": [
            "Setting",
            "Endpoint",
            "Who can change it",
            "What it does"
          ],
          "rows": [
            [
              "OIDC SSO",
              "GET/PUT/DELETE /v1/customer/sso/oidc",
              "org_admin (your own org)",
              "Configures generic OIDC single sign-on for your organization's login"
            ],
            [
              "SAML SSO",
              "GET/PUT/DELETE /v1/customer/sso/saml",
              "org_admin (your own org)",
              "Configures SAML 2.0 service-provider-initiated single sign-on"
            ]
          ]
        },
        "note": "Both require the Enterprise plan tier."
      },
      {
        "heading": "Workspaces",
        "paragraphs": [
          "Workspaces let you split your organization into team sub-groups, each of which can be assigned users and tracked separately for chargeback and budget purposes."
        ],
        "table": {
          "columns": [
            "Setting",
            "Endpoint",
            "Who can change it",
            "What it does"
          ],
          "rows": [
            [
              "Workspaces",
              "GET/POST/PATCH/DELETE /v1/customer/workspaces",
              "org_admin to create/edit/delete; any org role (with billing access) to view",
              "Creates and manages team sub-groups within your organization"
            ]
          ]
        },
        "note": "Requires the Workspaces feature (Standard tier and above)."
      },
      {
        "heading": "BYOK: bring your own provider keys",
        "paragraphs": [
          "You can supply your own API keys for providers like OpenAI or Anthropic, so your traffic is billed directly against your own provider account rather than NeuroRoute's platform key. This is useful if you have existing provider credits or negotiated provider-side rates.",
          "The My Provider Keys page (Billing section of the nav) shows a card per supported provider. Providers with a key already registered show a masked key preview and an active/valid/expired/invalid status badge; providers with no key show \"Not Set\" and an Add key button. Click Add key, pick the provider from the dropdown, paste the key, and Validate & save -- NeuroRoute validates it against the provider before storing it, so an invalid key is rejected immediately rather than silently failing on your first real request.",
          "Keys are encrypted at rest with AES-256-GCM envelope encryption and are never logged or displayed in full after saving -- only the last few characters are shown. Removing a key reverts that provider's traffic back to the platform key immediately."
        ],
        "table": {
          "columns": [
            "Setting",
            "Endpoint",
            "Who can change it",
            "What it does"
          ],
          "rows": [
            [
              "Customer provider keys (BYOK)",
              "GET/POST/DELETE /v1/customer/api-keys",
              "org_admin or org_billing (your own org)",
              "Registers your own provider API key so your traffic routes through it instead of the platform key"
            ]
          ]
        },
        "note": "Requires the BYOK feature (Premium tier and above)."
      },
      {
        "heading": "Event webhooks",
        "paragraphs": [
          "Event webhooks let you subscribe a single HTTP endpoint of your choosing to receive lifecycle notifications from NeuroRoute: budget.tripped (a budget cap was hit), org.erasure.completed (your erasure request finished executing), and key.created (a new API key was issued).",
          "Every webhook delivery is signed: the raw JSON body is HMAC-SHA256'd with the secret you configured, hex-encoded, and sent in an X-NeuroRoute-Signature: sha256=<hex> header, so your endpoint can verify the request genuinely came from NeuroRoute."
        ],
        "table": {
          "columns": [
            "Setting",
            "Endpoint",
            "Who can change it",
            "What it does"
          ],
          "rows": [
            [
              "Event webhooks",
              "GET/PUT/DELETE /v1/customer/event-webhooks",
              "org_admin (your own org)",
              "Subscribes one signed HTTP endpoint to budget.tripped, org.erasure.completed, and key.created events"
            ]
          ]
        },
        "note": "Requires the Webhooks feature (Premium tier and above)."
      },
      {
        "heading": "Alerts",
        "paragraphs": [
          "Alerts are threshold-based monitoring rules, distinct from event webhooks above: instead of being told a specific event happened, you're notified when a metric crosses a threshold you configure. Supported alert types are budget percentage, error rate, p95 latency, spend anomaly, provider health, and pricing drift. Each alert rule delivers via either email or a webhook channel, with a configurable cooldown between repeat firings.",
          "You can also list the history of alerts that have already fired for your organization."
        ],
        "table": {
          "columns": [
            "Setting",
            "Endpoint",
            "Who can change it",
            "What it does"
          ],
          "rows": [
            [
              "Alert rules",
              "GET/POST/PATCH/DELETE /v1/customer/alerts",
              "org_admin (your own org) to mutate; broader org roles to view",
              "Creates threshold-based alert rules (budget %, error rate, latency, spend anomaly, provider health, pricing drift) delivered by email or webhook"
            ],
            [
              "Alert event history",
              "GET /v1/customer/alerts/events",
              "Any org role",
              "Lists previously fired alert events and their delivery status"
            ]
          ]
        },
        "note": "Requires the Alerts feature (Premium tier and above)."
      },
      {
        "heading": "Prompt templates",
        "paragraphs": [
          "Prompt templates are reusable, versioned prompts you author once and invoke by slug from any client, instead of pasting the same instructions into every request. Invoke a template by setting \"model\": \"prompt:<slug>\" in your chat completion request, along with a \"variables\" object of key/value pairs to fill into the template -- the rendered messages replace whatever you would have sent, and the request then proceeds through the normal routing pipeline as usual."
        ],
        "code": {
          "lang": "bash",
          "text": "curl -X POST https://neuroroute.sislcloudworx.ai/v1/chat/completions \\\n  -H \"Authorization: Bearer nr_...\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"model\": \"prompt:summarize-ticket\",\n    \"variables\": {\"ticket_id\": \"1234\", \"tone\": \"formal\"}\n  }'"
        },
        "table": {
          "columns": [
            "Setting",
            "Endpoint",
            "Who can change it",
            "What it does"
          ],
          "rows": [
            [
              "Prompt templates",
              "GET/POST/DELETE /v1/customer/prompts, /prompts/{id}/versions",
              "org_admin (your own org) to mutate; broader org roles to view",
              "CRUD and versioning for reusable prompt templates, invoked via model: prompt:<slug>"
            ]
          ]
        }
      },
      {
        "heading": "Scheduled usage export",
        "paragraphs": [
          "You can have NeuroRoute automatically push a usage-data export on a recurring schedule instead of pulling it manually. The only supported cadences are off (empty string), daily, and weekly."
        ],
        "table": {
          "columns": [
            "Setting",
            "Endpoint",
            "Who can change it",
            "What it does"
          ],
          "rows": [
            [
              "Scheduled usage export",
              "GET/PUT /v1/customer/usage-export/schedule",
              "org_admin (your own org)",
              "Sets an automatic daily or weekly usage-export cadence (empty string turns it off)"
            ]
          ]
        }
      },
      {
        "heading": "Per-key budget caps",
        "paragraphs": [
          "When creating or updating an API key, you can set daily_budget_usd, monthly_budget_usd, and default_run_budget_usd to cap how much that specific key can spend. Leaving a field unset means unlimited for that window.",
          "Budget enforcement happens in a strict order, most-restrictive-wins: an agent run budget (if the request declares one) is checked first, then the per-key daily/monthly caps, then your organization's own daily/monthly caps, then a platform-wide guardrail. Whichever tier trips first blocks the request with a 402 response.",
          "A 402 response includes X-Budget-Tier (which tier tripped: run, key, org, or platform) and X-Budget-Window (daily or monthly) headers, along with X-Budget-Limit and X-Budget-Spent showing the actual cap and current spend, so you can see exactly what stopped the request without guessing."
        ],
        "table": {
          "columns": [
            "Setting",
            "Where it's set",
            "Who can change it",
            "What it does"
          ],
          "rows": [
            [
              "daily_budget_usd / monthly_budget_usd",
              "API key create (POST /v1/auth/keys) / update (PATCH /v1/auth/keys/{id})",
              "org_admin (your own org)",
              "Caps how much a single API key can spend per day or per month"
            ],
            [
              "default_run_budget_usd",
              "API key create (POST /v1/auth/keys) / update (PATCH /v1/auth/keys/{id})",
              "org_admin (your own org)",
              "Default ceiling for a single declared agent \"run\" using this key"
            ]
          ]
        },
        "note": "default_run_budget_usd requires the Run Budgets feature (Standard tier and above) to take effect."
      }
    ]
  }
];

/* ── Block renderer ─────────────────────────────────────── */
function DocCodeBlock({code}){
  if(!code) return null;
  return (
    <div style={{position:'relative',marginTop:8}}>
      <pre className="mono" style={{background:'var(--bg-2)',border:'1px solid var(--line)',borderRadius:10,padding:'14px 16px',fontSize:12.5,lineHeight:1.6,overflowX:'auto',margin:0}}>{code.text}</pre>
      <div style={{position:'absolute',top:8,right:8}}><CopyBtn text={code.text} label=""/></div>
    </div>
  );
}

function DocTable({table}){
  if(!table) return null;
  return (
    <div className="scroll-x" style={{marginTop:10}}>
      <table className="table">
        <thead><tr>{table.columns.map(function(c,i){return <th key={i}>{c}</th>;})}</tr></thead>
        <tbody>{table.rows.map(function(row,ri){
          return <tr key={ri}>{row.map(function(cell,ci){
            return <td key={ci} className={ci===0?'mono':''} style={ci===0?{whiteSpace:'nowrap'}:null}>{cell}</td>;
          })}</tr>;
        })}</tbody>
      </table>
    </div>
  );
}

function DocSection({section}){
  return (
    <div style={{marginBottom:28}}>
      <h3 style={{fontSize:16,fontWeight:700,margin:'0 0 10px',color:'var(--tx-0)'}}>{section.heading}</h3>
      {section.paragraphs && section.paragraphs.map(function(p,i){
        return <p key={i} style={{fontSize:13.5,lineHeight:1.65,color:'var(--tx-1)',margin:'0 0 10px'}}>{p}</p>;
      })}
      {section.list && <ul style={{margin:'0 0 10px',paddingLeft:20,fontSize:13.5,lineHeight:1.7,color:'var(--tx-1)'}}>
        {section.list.map(function(li,i){return <li key={i}>{li}</li>;})}
      </ul>}
      <DocTable table={section.table}/>
      <DocCodeBlock code={section.code}/>
      {section.note && <div className="row" style={{gap:8,marginTop:10,padding:'10px 12px',background:'rgba(255,193,7,.08)',border:'1px solid rgba(255,193,7,.25)',borderRadius:8,alignItems:'flex-start'}}>
        <Icon name="info" size={15} style={{color:'var(--amber)',flex:'none',marginTop:1}}/>
        <span style={{fontSize:12.5,lineHeight:1.6,color:'var(--tx-1)'}}>{section.note}</span>
      </div>}
    </div>
  );
}

// Topic deep-links ride the query string (#/docs?topic=<id>), never an extra
// hash path segment -- the app shell resolves PAGES off the FULL hash path
// (see app.jsx's PAGES[route] lookup), so "#/docs/routing" would look up a
// "docs/routing" page (none exists) and silently fall back to the dashboard.
function topicFromHash(){
  var parts = (window.location.hash.split('?')[1]||'').split('&');
  for(var i=0;i<parts.length;i++){
    var kv = parts[i].split('=');
    if(kv[0]==='topic') return decodeURIComponent(kv[1]||'');
  }
  return '';
}

function DocsPage(){
  var hashTopic = topicFromHash();
  var initial = DOC_TOPICS.some(function(t){return t.id===hashTopic;}) ? hashTopic : DOC_TOPICS[0].id;
  var _active=useState(initial); var active=_active[0], setActive=_active[1];

  var setTopic = function(id){
    setActive(id);
    window.location.hash = '#/docs?topic='+id;
  };

  useEffect(function(){
    var onHash = function(){
      var t = topicFromHash();
      if(DOC_TOPICS.some(function(d){return d.id===t;})) setActive(t);
    };
    window.addEventListener('hashchange', onHash);
    return function(){ window.removeEventListener('hashchange', onHash); };
  },[]);

  var topic = DOC_TOPICS.filter(function(t){return t.id===active;})[0] || DOC_TOPICS[0];

  return (
    <div className="grid" style={{gap:18}}>
      <PageHead desc="Reference documentation for integrating with and configuring NeuroRoute."/>
      <div style={{display:'grid',gridTemplateColumns:'220px minmax(0,1fr)',gap:18,alignItems:'start'}}>
        <div className="card" style={{padding:8,position:'sticky',top:18}}>
          {DOC_TOPICS.map(function(t){
            var isActive = t.id===active;
            return (
              <button key={t.id} onClick={function(){setTopic(t.id);}}
                className="row" style={{
                  width:'100%',textAlign:'left',gap:10,padding:'9px 10px',borderRadius:8,border:'none',cursor:'pointer',
                  background:isActive?'rgba(255,255,255,.08)':'transparent',
                  color:isActive?'var(--tx-0)':'var(--tx-2)',fontSize:13,fontWeight:isActive?600:500,marginBottom:2,
                }}>
                <Icon name={t.icon} size={15} style={{flex:'none'}}/>{t.title}
              </button>
            );
          })}
        </div>
        <SectionCard title={topic.title} sub={topic.summary}>
          {topic.sections.map(function(s,i){return <DocSection key={i} section={s}/>;})}
        </SectionCard>
      </div>
    </div>
  );
}

window.PAGES = Object.assign(window.PAGES||{}, {docs: DocsPage});
