# turning AI off (the runtime switch)

> An operator can switch an application's AI off while it runs — no recompile, no redeploy. Text generation and embedding are two independent switches, because they do different things and send different data. A refused call fails, naming which capability was off; it never silently returns nothing.

<!-- id: agent-ai-switch · area: agent · stability: stable · html: https://osysharp.com/reference/agent/ai-switch/ -->

## Summary        {#summary}
An operator can close an application's AI while it runs. Two switches, not one:
**text generation** and **embedding** are turned off independently. Neither needs a recompile, a redeploy or a
restart — the change reaches every process within about fifteen seconds.

A call made while its capability is off does not quietly return nothing. It fails, with a message naming which
capability was switched off and noting that the other is switched separately.

## Signature      {#signature}
```text
app-detail  →  AI
   ▢ Text generation — models may write for this app
   ▢ Embedding — semantic search, and every indexed row is sent to the model host
```

There is no Osy# declaration for this. It is deliberately not something an application can write about itself —
see [Who can change it](#who).

## Description    {#description}

### Why two switches and not one   {#two}
They differ on both axes that matter to whoever is turning AI off.

**What they do.** Generation samples text that influences something a person reads or acts on. An embedding is a
single forward pass producing a fixed-size vector: it *ranks*, it does not decide or write prose. It is still
inference — calling it "not AI" would not survive a compliance reading — but it is not what a rule about AI in
decisions is aimed at.

**What they send.** This is the one that catches people out. A generation call sends what somebody typed. Embedding
sends **every indexed row** to whoever hosts the model — much the larger flow of application data leaving the
platform. So a policy about *generated output* covers only the first, while a policy about *where data may go*
covers both.

Because only you know which policy you are applying, the platform models the two and composes neither. If you want
one "AI off" control, turn both off.

> ⛔ **"AI is off" is false while either one runs.** Turning generation off and reporting "we disabled AI" is a
> statement about data egress that is not true if embedding is still running. Both the admin page and the refusal
> message say which capability is which for exactly this reason.

### Who can change it   {#who}
The application's own **Owner or Admin**, its organization's **Owner or Admin**, or a platform admin. It is stored in
the control plane, not in the application's model, and that placement is the point: an operator's decision must not
be something the application's own source can withdraw at its next compile.

It is also the only field pair on an application record that an app's own Owner may write — everything else there
stays with the organization tier. The reasoning: this is not administration of the app record, it is an operational
pull on what the app *does*, and the person accountable for that must be able to stop it without escalating.

### What a refused call looks like   {#refused}
The call fails, and says why.

**A model call** — `Llm.Complete`, `Llm.Extract`, `Llm.Decide`, an agent's work — is made by `Osysharp.Llm`
([The Llm kit — call a model, and know what each call cost](https://osysharp.com/reference/agent/llm-calls/)), which asks the switch before anything is sent and throws `LlmSwitchedOff`: *"Generative AI is
switched off for this application by its operator. Nothing was sent."* It is an `LlmRefused`, so the one `catch` an
app writes for a model that will not answer covers it too.

**An embedding** — indexing a `[Searchable]` field, or a search by meaning — is refused by the platform. Over HTTP
the response is **403** with the message:

```json title="embedding switched off" syntax
{ "error": "Embedding is switched off for this application — an operator disabled the Embedding AI capability, so no vector was computed and semantic search over `[Searchable]` fields cannot answer. This is not a budget limit and will not clear on its own. Text generation is switched separately and may still be running." }
```

403 rather than 500, because this is a decision somebody made rather than a fault; and rather than 429, because a
budget refusal clears when its window turns and invites a retry while this one does not clear at all.

⚠ **The generation switch closes `Osysharp.Llm`.** Every model call made through the kit — which is every call the
kit's providers, the agents kit and the translation kits make — asks it. Code that reaches a model's HTTP API by
itself, with `Http.Post` and its own key, is an outbound request like any other and is not stopped by this switch;
what stops that is the application's egress rules.

### When it takes effect   {#timing}
Within about **fifteen seconds**, in every process. The switch is read on every AI call, so it is cached briefly
rather than re-read from the control plane each time; the cache is time-bounded, so a change propagates everywhere
with nothing to deliver and nothing to go wrong in delivering it.

So "it is off" and "it will be off shortly" are different claims for a few seconds after you flip it. The admin page
says so.

### What it does NOT do   {#limits}
Two things worth knowing before you design around this.

**An application CAN ask whether AI is on** — `Ai.GenerationEnabled` and `Ai.EmbeddingEnabled`, two booleans read
on the server:

```osy title="hide the affordance instead of failing on it" test app=agent-ai-switch-hide
using Osysharp.Ui;

// The read runs on the SERVER — it needs the host's own authority — so it lives in a top-level function the page
// calls, not in the page. `[AllowAnonymous]` because a public page's visitor has no credential, and who may SEE a
// page and who may CALL a function are separate declarations.
[AllowAnonymous]
bool AiIsOn() { return Ai.GenerationEnabled; }

[Route("/assistant")]
[Render(CSR)]
[AllowAnonymous]
component Assistant() {
  bool aiOn = false;

  // On mount, not in the render: a render is evaluated synchronously and cannot wait for the server. That is also
  // exactly why this is read WHEN THE PAGE LOADS rather than pushed — the staleness the paragraph below is about.
  on mount {
    aiOn = AiIsOn();
  }

  render {
    if (aiOn) { Text("Ask the assistant anything."); }
    else      { Text("The assistant is switched off for this application."); }
  }
}
```

⛔ **On a page this is advisory, never a gate.** The authority is the refusal above: the read has crossed to a client and can
be up to fifteen seconds stale, so a page that hides a button is being polite, not enforcing anything. What makes
the pair trustworthy is that both read the same switch — the button and the refusal cannot disagree about the
answer, only about how fresh it is.

⚠ And it is read when the page loads, not pushed. A page already open when the switch is flipped keeps showing the
button until it reloads; pressing it then gives the refusal. If you need the page to react live, that is a
different mechanism.

**The embedding refusal is not a catchable Osy# exception.** It is not a member of the language's closed exception
set, so `catch (…)` in Osy# cannot name it specifically; it surfaces as a call failure carrying the message above.
The model-call refusal is an ordinary one: `catch (LlmSwitchedOff)`, or `catch (LlmRefused)` with the others.

**Rows already embedded were already sent.** Turning embedding off stops future calls; it does not recall anything.
If the concern is where data has gone rather than where it is going, this switch is not the whole answer — see
[embedding provider (app.Embedding)](https://osysharp.com/reference/config/embedding/) for running the model somewhere you choose instead.

## Examples       {#examples}

An operator turns generation off for one application and leaves search working:

```text title="one capability off, the other left running"
app-detail → AI
   ▢ Text generation      ← unticked
   ☑ Embedding
   "Generation is off. Embedding still runs, so indexed rows are still sent to whoever hosts that model."
```

The application's next model call is refused with `LlmSwitchedOff`. Its semantic search keeps answering.

### What the host counts   {#counted}
The host keeps its own count of AI work across everything it serves — model calls, their tokens and what they
cost, and how long work waited for a person. It bounds nothing and is never billed; it is what tells an operator how
much of what a fleet does is AI work at all, and what an application's owner reads on the console's usage page. The
work itself happens in Osy#, so the code that does it says so:

```osy syntax
Ai.CountGeneration(tokens);               // one model call, and the tokens the provider counted for it
Ai.CountGeneration(tokens, costMicros);   // …and what it cost, in millionths of a US dollar
Ai.CountHumanWait(seconds);               // one hand-off to a person has its answer, after this long
```

`Osysharp.Llm` calls the second for every answered attempt, priced from the app's own price book, so an app that
calls models through the kit writes none of these lines. A provider or a kit of your own that parks work for a person calls `Ai.CountHumanWait` when the wait ends; a
negative wait, or one longer than ninety days, is not counted.

### The dial beside the switch   {#budget}
The switch is binary. What a model call may *spend* is a dial, and it is the application's own: `Osysharp.Llm`
keeps budgets per scope and window, reserved before a call leaves ([Model spend — budgets, the meter and the price book (Llm kit)](https://osysharp.com/reference/agent/llm-spend/)). An application that
handles the switch's refusal handles a budget's the same way — both are `LlmRefused`, the model not answering, and
the degraded path is one path. Embedding has daily limits of its own, set per organisation and application in the
control plane.

## See also       {#see-also}
- [Model spend — budgets, the meter and the price book (Llm kit)](https://osysharp.com/reference/agent/llm-spend/) — the dial beside this switch: budgets per scope and window, kept by `Osysharp.Llm`
- [The Llm kit — call a model, and know what each call cost](https://osysharp.com/reference/agent/llm-calls/) — the models that are being switched off, and the call that asks the switch
- [embedding provider (app.Embedding)](https://osysharp.com/reference/config/embedding/) — the embedding model, its endpoint, and why placement is a requirement rather than a preference
- [Agents (a class with a job, its own account, a budget and a record)](https://osysharp.com/reference/agent/index/) — the rest of the agent and model surface
