Osy#betaRun it on Osyrin: osyrin.com

Reference / Agent

turning AI off (the runtime switch)

app-detail → AI: Text generation ▢ · Embedding ▢

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.

stable1 example compiled by CIagentaioperationssecurity

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#

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.

Description#

Why two switches and not one#

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#

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#

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), 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:

{ "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#

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#

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:

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) for running the model somewhere you choose instead.

Examples#

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

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#

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:

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#

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)). 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#

Related

The Llm kit — call a model, and know what each call cost

`Osysharp.Llm` is the road every model call takes, written in Osy#: the operator's AI switch, the app's router, a…

Model spend — budgets, the meter and the price book (Llm kit)

The Llm kit keeps three things about what model calls cost. The METER: one `ModelCall` row for every attempt, with the…

embedding provider (app.Embedding)

`app.Embedding` declares the embedding model the app uses to turn text into vectors for semantic search over…

Agents (a class with a job, its own account, a budget and a record)

An agent is a class of the `Osysharp.Agents` kit: a purpose, the skills it holds and what it may spend. A skill is a…