The call-time check every provider-calling verb passes before its HTTP
request: does the model behind the endpoint (or the call's model:
override) have what the call and its task need.
The needs of a call are its task's declared needs together with what
the call itself shows (implied_needs/2):
| verb | implied |
|---|---|
complete/3, ask/3, ask_with_prompt/4, complete_with_system_prompt/5 | image_url parts in the messages → :vision; file parts → :file_input (string or atom keys either way); tools: → :tools; schema: / json: → :json |
describe_image/3 | :vision; :json with schema: / json: |
extract_text/3, compare_images/4 | :vision, :json |
generate_image/3, edit_image/4, process_image/4 | :image_output (images given to a generation model are not vision) |
embed/3, speak/3 | nothing |
The model checked is the one the verb sends: a model: override of the
vision and image verbs, the endpoint's model otherwise.
A hard need (:vision, :file_input, :image_output, :tools,
:json_schema) the model says no to
refuses the call with {:error, {:model_unfit, missing}}. A need nobody
knows about lets the call through, unless the module setting
ai_guard_verified_models_only is on — then it refuses with
{:error, {:model_unverified, unclear}}. :json is soft: a model without
it gets the call (the schema is written into the prompt), and the usage
row notes guard_warnings: ["json"].
With a hard :tools / :json_schema need, a call to OpenRouter asks it
to route only to upstream providers that support every parameter sent
(provider: %{require_parameters: true}): the catalogue's
supported_parameters are a union over providers, so without it a fit
model could still land on a provider that ignores the schema. The
request parameters the model's catalogue entry does not list are then
left out (the row notes them as stripped_params), since one of them
would leave no provider at all. Without the model's parameter list in the
cache (before the first catalogue read after a restart, or a model the
catalogue lacks) the request goes as it did before: no flag, nothing left
out.
Admission for a whole turn
A caller that makes several calls for one piece of work (an assistant's
turn) asks once, with admit/4: the task's limits are checked then, and
the answer is an admission token kept on this node for ttl_s seconds.
Its calls pass guard_admission: token and skip the limit check — so the
work that started under the limits runs to its end — but each is still
checked for fitness, and for PhoenixKitAI.Budget. A token is honoured
only for the task and user it was issued for, and only before it expires;
any other token is ignored and the call is checked as usual.
A blocked person's turn is refused at the admission
({:error, {:task_blocked, group}}); a turn admitted before the block
runs to its end.
Off-purpose use
The consumer flags it; the guard never edits a prompt or a schema. A
consumer asks self_flag?/1 before it builds its prompt and schema, puts
the text of self_flag_instruction/1 into its system prompt (with its own
way to flag: a field of its JSON schema, a marker in the answer), and
calls report_off_purpose/4 when the model flags a request. The flag is
an event per turn or call; the watchers are told once a day per person and
task, without the reason (PhoenixKitAI.GuardEvents).
A task may also declare norms that need no content (sources,
max_calls_per_hour, max_tokens_per_call): after a call's journal row
is written, check_anomalies/1 compares the row with them and signals a
broken one once a day (anomaly). It never blocks.
A person is blocked by a person (block/5, unblock/3, see
PhoenixKitAI.GuardBlocks); there is no automatic block.
Who may change the guard
ai.manage (a sub-permission of ai) — or an Owner, always:
may_manage?/1. Every function that changes the guard's settings takes
the scope of the person doing it and checks it (authorize_manage/1).
The reasons of a private task's flags and blocks are shown only to them
(reasons_visible?/2).
Summary
Functions
Admits user_uuid (nil for none) to task for ttl_s seconds: checks the
task's blocks and limits once and answers {:ok, token} for the calls of
this piece of work to pass as guard_admission: (see the moduledoc), or
the refusal ({:error, {:task_blocked, group}}, or
{:error, {:task_limit_exceeded, scope, unit, period}}, which is
signalled like a refused call). turn_ref is kept with the token.
Whether token is an admission this node issued for task and
user_uuid that has not expired.
Blocks a person from a task's group: PhoenixKitAI.GuardBlocks.block/5.
Whether user_uuid is blocked from task_key now (for a page to show it).
The fitness part of the call-time check: {:ok, admission} when the
model may take the call, {:error, reason} when it may not. task is
the call's declaration (or nil); implied what the call shows.
Compares a call's journal row with its task's anomaly rules and signals
each broken one, once a day per task, person and rule
(PhoenixKitAI.GuardEvents.anomaly/6)
What a call shows it needs, by verb (see the table above). input is the
messages for :complete; other verbs ignore it.
The permission that changes the guard.
Whether scope may change the guard: an Owner, or a holder of ai.manage.
Whether scope sees the reason (and the journal row) of a flag or a
block: every viewer of the page for a task that is not private, a holder
of ai.manage or an Owner for a private one.
Reports that the model flagged a request of task by user_uuid (nil for
none) as off-purpose. ref is the call's journal row uuid
("request_uuid" of a chat verb's answer, :request_uuid of an image
verb's), or a map with turn_ref (one flag per turn of several calls).
A string that is not a uuid is taken as a turn reference. reason is the
model's, cut to 500 characters.
The call's request options for an admission that asks OpenRouter to
require its parameters: :require_parameters set, and every option the
model's catalogue entry does not list left out. Returns the options and
the names left out. Without the flag (which is set only with a known
parameter list) the options are returned as they are.
What the guard adds to a call's usage row: the task, and the warnings and stripped parameters when there are any.
Whether the consumer of task (a key or a declaration) should have its
model flag off-purpose requests: the task's guard mode when an admin set
one (self_flag or self_flag+classifier), else the declaration's
self_flag. An unknown task is false. Costs no query once the task's
settings are cached.
An English instruction a consumer can put into its system prompt: the task's purpose (an admin's override when set), to refuse an off-topic request politely and flag it, to judge only the user's own message, and to give the reason without quoting the user. The consumer adds how to flag (its schema's field, a marker). nil for an unknown task.
Turns "only verified models" on or off. Needs ai.manage; written to the
Activity log.
%{task: key} for a call that names a task, %{} otherwise — for a row written before any admission.
Lifts a block: PhoenixKitAI.GuardBlocks.unblock/3.
Whether a call with an unknown capability is refused (module setting, default off).
The module setting behind verified_only?/0.
Types
@type verb() ::
:complete
| :embed
| :speak
| :generate_image
| :edit_image
| :process_image
| :describe_image
| :extract_text
| :compare_images
Functions
@spec admit(String.t(), String.t() | nil, term(), pos_integer()) :: {:ok, String.t()} | {:error, term()}
Admits user_uuid (nil for none) to task for ttl_s seconds: checks the
task's blocks and limits once and answers {:ok, token} for the calls of
this piece of work to pass as guard_admission: (see the moduledoc), or
the refusal ({:error, {:task_blocked, group}}, or
{:error, {:task_limit_exceeded, scope, unit, period}}, which is
signalled like a refused call). turn_ref is kept with the token.
Without the request cache's flags table (the cache not running) the token
is not kept: admitted?/3 says no to it, and each call is checked as if
it had none.
Whether token is an admission this node issued for task and
user_uuid that has not expired.
Blocks a person from a task's group: PhoenixKitAI.GuardBlocks.block/5.
Whether user_uuid is blocked from task_key now (for a page to show it).
@spec check(PhoenixKitAI.Endpoint.t(), keyword(), PhoenixKitAI.TaskSpec.t() | nil, [ PhoenixKitAI.TaskSpec.need() ]) :: {:ok, admission()} | {:error, term()}
The fitness part of the call-time check: {:ok, admission} when the
model may take the call, {:error, reason} when it may not. task is
the call's declaration (or nil); implied what the call shows.
@spec check_anomalies(PhoenixKitAI.Request.t()) :: :ok
Compares a call's journal row with its task's anomaly rules and signals
each broken one, once a day per task, person and rule
(PhoenixKitAI.GuardEvents.anomaly/6):
sources— the row'ssourceis not one of them (or a function of one:"My.Module"allows"My.Module.run");max_tokens_per_call— the row has more tokens;max_calls_per_hour— the person made more calls of the task in the last hour (rows with a user only; refusals are not calls).
A refused row, a row without a task, and a task without rules cost nothing; the hourly count is skipped once its rule was signalled today. Never fails the caller.
@spec implied_needs(verb(), keyword(), term()) :: [PhoenixKitAI.TaskSpec.need()]
What a call shows it needs, by verb (see the table above). input is the
messages for :complete; other verbs ignore it.
@spec manage_permission() :: String.t()
The permission that changes the guard.
Whether scope may change the guard: an Owner, or a holder of ai.manage.
Whether scope sees the reason (and the journal row) of a flag or a
block: every viewer of the page for a task that is not private, a holder
of ai.manage or an Owner for a private one.
@spec report_off_purpose(String.t(), String.t() | nil, term(), String.t() | map()) :: {:ok, PhoenixKitAI.GuardEvent.t()} | :duplicate | :off | {:error, :unknown_task | :invalid_ref | :invalid_user | :unknown_user | :not_recorded}
Reports that the model flagged a request of task by user_uuid (nil for
none) as off-purpose. ref is the call's journal row uuid
("request_uuid" of a chat verb's answer, :request_uuid of an image
verb's), or a map with turn_ref (one flag per turn of several calls).
A string that is not a uuid is taken as a turn reference. reason is the
model's, cut to 500 characters.
One flag per turn or call: {:ok, event} for the first, :duplicate
after. :off when the task does not self-flag now (self_flag?/1);
{:error, :unknown_task | :invalid_ref | :invalid_user | :unknown_user}
otherwise, and {:error, :not_recorded} when the database fails (logged
as its class; the caller's work goes on). The watchers are told of a person's first flag in the task
each day, without the reason (PhoenixKitAI.GuardEvents.off_purpose/5).
The call's request options for an admission that asks OpenRouter to
require its parameters: :require_parameters set, and every option the
model's catalogue entry does not list left out. Returns the options and
the names left out. Without the flag (which is set only with a known
parameter list) the options are returned as they are.
What the guard adds to a call's usage row: the task, and the warnings and stripped parameters when there are any.
@spec self_flag?(String.t() | PhoenixKitAI.TaskSpec.t()) :: boolean()
Whether the consumer of task (a key or a declaration) should have its
model flag off-purpose requests: the task's guard mode when an admin set
one (self_flag or self_flag+classifier), else the declaration's
self_flag. An unknown task is false. Costs no query once the task's
settings are cached.
@spec self_flag_instruction(String.t() | PhoenixKitAI.TaskSpec.t()) :: String.t() | nil
An English instruction a consumer can put into its system prompt: the task's purpose (an admin's override when set), to refuse an off-topic request politely and flag it, to judge only the user's own message, and to give the reason without quoting the user. The consumer adds how to flag (its schema's field, a marker). nil for an unknown task.
The text does not follow the locale, so the prompt stays the same.
Turns "only verified models" on or off. Needs ai.manage; written to the
Activity log.
%{task: key} for a call that names a task, %{} otherwise — for a row written before any admission.
Lifts a block: PhoenixKitAI.GuardBlocks.unblock/3.
@spec verified_only?() :: boolean()
Whether a call with an unknown capability is refused (module setting, default off).
@spec verified_only_setting() :: String.t()
The module setting behind verified_only?/0.