{% extends "admin/_layout.html" %} {% from "_macros.html" import icon, model_avatar %} {% set section = "models" %} {% block title %}{{ model.label }} - Models - {{ brand.name }}{% endblock %} {% block heading %}{{ model.label }}{% endblock %} {% block admin_content %} {% if saved %}
{{ icon("check", "icon--sm") }} {{ saved }}
{% endif %}
{{ model_avatar(model, cls="model-detail__avatar") }}
{{ model.label }}
{{ model.model_id }}
via {{ model.connection.name }}
{% if model.model_id == default_model %} {{ icon("star", "icon--sm") }} default model {% else %}
{% endif %}
{% if model.image_path %}
{% endif %}

{{ t("PNG, JPEG, WEBP or GIF, under 2 MB. Without one, the model gets a generated badge whose colour is derived from its id.") }}

{# Empty, hidden, and outside every other form: the Detect button further down is associated with it by `form="detect-efforts"`. It carries no fields on purpose — detection asks the endpoint and needs nothing from this page. #}

{{ t("Presentation") }}

{{ t("Shown instead of the raw id. Empty uses the id.") }}

Place in the picker, 1–{{ total }}. Typing a number is the workable way to move a model a long distance.

{{ t("How many tokens this model can hold, filled in from the endpoint where it says. Leave it empty if you do not know: the context percentage and automatic compaction both stay off rather than working from a guess.") }}

{{ t("Reasoning efforts this model accepts") }}
{% for value in efforts %} {% endfor %}
{% if detected %}
{{ icon('sparkle' if detected == 'success' else 'warning', 'alert__icon') }} {{ detected_message }}
{% endif %} {# Reading the answer rather than asking somebody to know it. llama-server publishes the loaded model's Jinja chat template on `/props`, and that template is the thing that rejects an effort it does not recognise -- so the accepted set is written down in the one authoritative place. Endpoints without that route (OpenAI, vLLM) say so rather than pretending the model accepts nothing. Its own form, because this page's main form is a PUT of everything and a detect must not carry half-edited fields with it — and that form is declared before the main one rather than here, with this button reaching it by id. 🚨 It was written inline here, nested inside the main form, which HTML does not allow. Nothing complains: the parser *drops* the inner `form` start tag and then lets the matching end tag close the outer one — so from this point down the page was in no form at all. "Save changes" submitted nothing; the description, the system prompt, every capability and the whole availability card could not be saved. And this button submitted the main form's surviving half to the *save* route, where every field it did not carry took its default: description cleared, system prompt cleared, and the model disabled with all of its tools off. Shipped in 1.3.0 and found in 1.3.2 by asking a browser which form each control belonged to, which is the only thing that finds it — the markup reads correctly, and a test posting to the route bypasses the parser entirely. `tests/test_form_structure.py` is the guard. #}

The vocabulary is {{ t("not the same for every model") }}, and sending one a model does not know is not ignored — it is rendered into the model's chat template, which raises and fails the whole reply. gpt-oss takes {{ t("low/medium/high") }}; Bonsai takes {{ t("low/medium/xhigh") }} and refuses {{ t("high") }}; OpenAI has added {{ t("minimal") }}, {{ t("xhigh") }} and {{ t("max") }} at various points.
Tick none and the common three are offered, which is right for almost everything. If an endpoint ever refuses one anyway, that reply is retried without it and this list corrects itself — so this is worth setting by hand only to save that one round trip.

A {{ t("seed") }}, not a per-request setting: it is copied onto a chat when the chat is created and when somebody switches to this model, and from then on the chat's own value is what is sent. Changing it here therefore does nothing to chats that already exist. The composer's picker shows whichever level is actually in force, so what somebody sees there is what goes out. Anyone can change it per chat with {{ t("/effort") }}, and the control only appears on a model marked {{ t("Reasoning") }} above.
Sent two ways at once, because there is no one field that works: OpenAI and vLLM read {{ t("reasoning_effort") }}, while llama.cpp drops it silently and reads only {{ t("chat_template_kwargs") }} — which is the route by which it reaches gpt-oss. Both go out, and only on a chat that has an effort set, so an endpoint strict about unknown parameters is untouched until somebody chooses one.

Shown in the chat settings panel and your users' settings — and, if any model here may ask another one a question, read by those models too.

{{ t("Never shown to a person. It goes into the list of the other models that a model sees when it is allowed to ask one of them a question, so write what would help it choose: size, what this one is good and bad at, a score you trust. Leave it empty and the description above carries that on its own.") }}

{{ t("System prompt") }}

{{ t("Used by chats on this model that have no prompt of their own. A chat's own prompt overrides this; this overrides the instance prompt.") }}

{% if instance_prompt %} Leave empty to fall back to the instance prompt shown above. {% else %} Leave empty to send no system prompt. {% endif %}

{{ t("Capabilities") }}

What this endpoint can do. Endpoints rarely advertise it reliably, so it is your call. {{ t("reasoning") }} shows the thinking block, {{ t("vision") }} lets images be sent, and {{ t("tools") }} is whether a tool list may be sent at all — turn it on for a model that does not support tool calling and every one of its replies fails. {{ t("embeddings") }} is the odd one out: it says this is not a chat model at all, but one that turns text into vectors, and it is what Extraction picks from.

{% for name in capabilities %} {% endfor %}

Built-in tools {% if not (model.capabilities_json or {}).get("tools") %} {{ t("needs tools") }} {% endif %}

What this model is given, as opposed to what it is capable of. None of it applies unless {{ t("tools") }} is ticked above. Each one is also subject to the instance being configured for it and to the reader's own permissions — this only decides whether it is offered to {{ t("this") }} model.

{# A model configured before these existed has no tool_* keys. Ticked by default when `tools` is on, matching what the tool registry does, so the form shows what will actually happen rather than a row of empty boxes that would take web search away on the next save. #} {% for key, label in tool_capabilities %} {% endfor %}

{{ t("Availability") }}

{{ t("Uncheck to restrict this model to chosen groups. Administrators always have access.") }}

{{ t("Groups with access") }} {% if groups %}
{% for group in groups %} {% endfor %}

{{ t("Ignored while the model is available to everyone.") }}

{% else %}

No groups yet — create one to restrict access.

{% endif %}
Back to all models
{# Outside the form above, and it has to be: two forms cannot nest, and this one posts somewhere else. See the note beside the Detect button. #}

{{ t("Default personality") }}

Who this model is before it has worked out who it is with somebody. Different from the system prompt above: that is an instruction you write, this is a character it can be — and, with {{ t("Edit its own personality") }} ticked, one it rewrites for itself.

{{ t("A personality belongs to a person.") }} Each account gets its own version of this model's character, starting from what you write here and diverging from it the first time the model writes its own. Changing this afterwards does not reach anybody who already has one, and it is not stacked underneath theirs — two personalities at once would contradict each other and nobody could tell which was losing. What the model *is*, as opposed to who it has become with somebody, belongs in {{ t("Description") }} and {{ t("Facts for other models") }} above, which are the same for everyone.

Up to {{ persona_limit }} characters, in the first person. Empty removes it and its history. It is sent on every request, so length here costs the same as length in the system prompt.

{% if persona and persona.author == "model" %} {{ t("last written by the model") }} {% elif persona %} {{ t("last written here") }} {% endif %}
{% if persona and persona.revisions %}

Earlier defaults {{ persona.revisions|length }}

{{ t("What this default said before each change. Each person's own personality keeps its own history, which they can see and restore in their own settings — this is the starting point's history, not theirs.") }}

{% endif %} {% endblock %}