90623b461d
A column rather than a key in capabilities_json, which is rebuilt wholesale from the submitted checkboxes on every save and would destroy a number living in it. 0 means unknown, and unknown has to stay tellable from small: the context percentage and automatic compaction both refuse to act on a figure nobody supplied. Filled in from /v1/models where the runner advertises it -- OpenRouter, vLLM and llama.cpp each spell it differently, so context_from() reads the four spellings actually in use, accepts a quoted number but not "8192 tokens", and rejects anything outside 256..10,000,000. Applied on discovery only when nothing is set: a refresh must never undo a correction, since an administrator sets this precisely because the endpoint was wrong. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
239 lines
9.2 KiB
HTML
239 lines
9.2 KiB
HTML
{% extends "admin/_layout.html" %}
|
||
{% from "_macros.html" import icon, model_avatar %}
|
||
{% set section = "models" %}
|
||
|
||
{% block title %}{{ model.label }} - Models - LLeMbas{% endblock %}
|
||
{% block heading %}{{ model.label }}{% endblock %}
|
||
|
||
{% block admin_content %}
|
||
<nav class="crumbs">
|
||
<a href="/admin/models">{{ icon("chevron-right", "icon--sm crumbs__back") }} All models</a>
|
||
<span class="spacer"></span>
|
||
{# Walking the list without going back to it, which is what you want when
|
||
tidying up a freshly imported connection. #}
|
||
{% if previous %}
|
||
<a class="btn btn--sm" href="/admin/models/{{ previous.id }}/edit"
|
||
title="{{ previous.label }}">{{ icon("arrow-up", "icon--sm") }} Previous</a>
|
||
{% endif %}
|
||
<span class="text-xs faint">{{ position_of }} of {{ total }}</span>
|
||
{% if next %}
|
||
<a class="btn btn--sm" href="/admin/models/{{ next.id }}/edit"
|
||
title="{{ next.label }}">Next {{ icon("arrow-down", "icon--sm") }}</a>
|
||
{% endif %}
|
||
</nav>
|
||
|
||
{% if saved %}
|
||
<div class="alert alert--success">{{ icon("check", "icon--sm") }} <span>{{ saved }}</span></div>
|
||
{% endif %}
|
||
|
||
<section class="card">
|
||
<div class="card__header">
|
||
<div class="row" style="gap: var(--sp-3); min-width: 0">
|
||
{{ model_avatar(model, cls="model-detail__avatar") }}
|
||
<div style="min-width: 0">
|
||
<strong>{{ model.label }}</strong>
|
||
<div><code class="text-xs faint">{{ model.model_id }}</code></div>
|
||
<div class="text-xs faint">via {{ model.connection.name }}</div>
|
||
</div>
|
||
</div>
|
||
<div class="btn-row">
|
||
{% if model.model_id == default_model %}
|
||
<span class="badge badge--leaf">{{ icon("star", "icon--sm") }} default model</span>
|
||
{% else %}
|
||
<form method="post" action="/admin/models/{{ model.id }}/default">
|
||
<button class="btn btn--sm" type="submit">
|
||
{{ icon("star", "icon--sm") }} Make default
|
||
</button>
|
||
</form>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="btn-row">
|
||
<form method="post" action="/admin/models/{{ model.id }}/image"
|
||
enctype="multipart/form-data" class="btn-row">
|
||
<input class="input input--file" type="file" name="image"
|
||
accept="image/png,image/jpeg,image/webp,image/gif" required
|
||
aria-label="Model image">
|
||
<button class="btn btn--sm" type="submit">{{ icon("image", "icon--sm") }} Upload image</button>
|
||
</form>
|
||
{% if model.image_path %}
|
||
<form method="post" action="/admin/models/{{ model.id }}/image/delete">
|
||
<button class="btn btn--sm btn--danger" type="submit">Remove image</button>
|
||
</form>
|
||
{% endif %}
|
||
</div>
|
||
<p class="field__hint">
|
||
PNG, JPEG, WEBP or GIF, under 2 MB. Without one, the model gets a generated
|
||
badge whose colour is derived from its id.
|
||
</p>
|
||
</section>
|
||
|
||
<form method="post" action="/admin/models/{{ model.id }}">
|
||
<section class="card">
|
||
<h2 class="card__title">Presentation</h2>
|
||
|
||
<div class="grid grid--2">
|
||
<div class="field">
|
||
<label class="field__label" for="display-name">Display name</label>
|
||
<input class="input" id="display-name" name="display_name"
|
||
value="{{ model.display_name }}" placeholder="{{ model.model_id }}">
|
||
<p class="field__hint">Shown instead of the raw id. Empty uses the id.</p>
|
||
</div>
|
||
|
||
<div class="field">
|
||
<label class="field__label" for="position">Position</label>
|
||
<input class="input" id="position" name="position" type="number" min="1"
|
||
max="{{ total }}" value="{{ position_of }}">
|
||
<p class="field__hint">
|
||
Place in the picker, 1–{{ total }}. Typing a number is the workable way
|
||
to move a model a long distance.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="field">
|
||
<label class="field__label" for="context-length">Context length</label>
|
||
<input class="input" id="context-length" name="context_length" type="number"
|
||
min="0" step="1" placeholder="unknown"
|
||
value="{{ model.context_length or '' }}">
|
||
<p class="field__hint">
|
||
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.
|
||
</p>
|
||
</div>
|
||
|
||
<div class="field">
|
||
<label class="field__label" for="description">Description</label>
|
||
<textarea class="textarea" id="description" name="description" rows="2"
|
||
placeholder="What is this model good at?">{{ model.description }}</textarea>
|
||
<p class="field__hint">Shown in the chat settings panel and your users' settings.</p>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="card">
|
||
<h2 class="card__title">System prompt</h2>
|
||
<p class="card__lede">
|
||
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.
|
||
</p>
|
||
<div class="field">
|
||
<label class="field__label visually-hidden" for="system-prompt">System prompt</label>
|
||
<textarea class="textarea" id="system-prompt" name="system_prompt" rows="5"
|
||
placeholder="{{ instance_prompt or 'No instance prompt is set.' }}"
|
||
>{{ model.system_prompt }}</textarea>
|
||
<p class="field__hint">
|
||
{% if instance_prompt %}
|
||
Leave empty to fall back to the instance prompt shown above.
|
||
{% else %}
|
||
Leave empty to send no system prompt.
|
||
{% endif %}
|
||
</p>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="card">
|
||
<h2 class="card__title">Capabilities</h2>
|
||
<p class="card__lede">
|
||
What this endpoint can do. Endpoints rarely advertise it reliably, so it
|
||
is your call. <strong>reasoning</strong> shows the thinking block,
|
||
<strong>vision</strong> lets images be sent, and <strong>tools</strong> 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.
|
||
</p>
|
||
<div class="checkbox-row">
|
||
{% for name in capabilities %}
|
||
<label class="checkbox">
|
||
<input type="checkbox" name="capability" value="{{ name }}"
|
||
{{ 'checked' if (model.capabilities_json or {}).get(name) }}>
|
||
<span>{{ name }}</span>
|
||
</label>
|
||
{% endfor %}
|
||
</div>
|
||
</section>
|
||
|
||
<section class="card">
|
||
<h2 class="card__title">
|
||
Built-in tools
|
||
{% if not (model.capabilities_json or {}).get("tools") %}
|
||
<span class="badge">needs tools</span>
|
||
{% endif %}
|
||
</h2>
|
||
<p class="card__lede">
|
||
What this model is given, as opposed to what it is capable of. None of it
|
||
applies unless <strong>tools</strong> 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 <em>this</em>
|
||
model.
|
||
</p>
|
||
<div class="checkbox-row">
|
||
{# 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 %}
|
||
<label class="checkbox">
|
||
<input type="checkbox" name="capability" value="{{ key }}"
|
||
{{ 'checked' if (model.capabilities_json or {}).get(key, tool_default) }}>
|
||
<span>{{ label }}</span>
|
||
</label>
|
||
{% endfor %}
|
||
</div>
|
||
</section>
|
||
|
||
<section class="card">
|
||
<h2 class="card__title">Availability</h2>
|
||
|
||
<div class="field">
|
||
<div class="checkbox-row">
|
||
<label class="checkbox">
|
||
<input type="checkbox" name="enabled" value="true" {{ 'checked' if model.enabled }}>
|
||
<span>Enabled — offered in chats</span>
|
||
</label>
|
||
<label class="checkbox">
|
||
<input type="checkbox" name="pinned" value="true" {{ 'checked' if model.pinned }}>
|
||
<span>Pinned — shortcut in the chat sidebar</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="field">
|
||
<label class="checkbox">
|
||
<input type="checkbox" name="public" value="true" {{ 'checked' if model.public }}>
|
||
<span>Available to everyone</span>
|
||
</label>
|
||
<p class="field__hint">
|
||
Uncheck to restrict this model to chosen groups. Administrators always
|
||
have access.
|
||
</p>
|
||
</div>
|
||
|
||
<div class="field">
|
||
<span class="field__label">Groups with access</span>
|
||
{% if groups %}
|
||
<div class="checkbox-row">
|
||
{% for group in groups %}
|
||
<label class="checkbox">
|
||
<input type="checkbox" name="group_ids" value="{{ group.id }}"
|
||
{{ 'checked' if group in model.groups }}>
|
||
<span>{{ group.name }}</span>
|
||
</label>
|
||
{% endfor %}
|
||
</div>
|
||
<p class="field__hint">Ignored while the model is available to everyone.</p>
|
||
{% else %}
|
||
<p class="field__hint">
|
||
No groups yet — <a href="/admin/groups">create one</a> to restrict access.
|
||
</p>
|
||
{% endif %}
|
||
</div>
|
||
</section>
|
||
|
||
<div class="btn-row">
|
||
<button class="btn btn--primary" type="submit">Save changes</button>
|
||
<a class="btn btn--ghost" href="/admin/models">Back to all models</a>
|
||
</div>
|
||
</form>
|
||
{% endblock %}
|