Files
LLeMbas/src/lembas/web/templates/admin/model_detail.html
T
Jaroslav Beneš 90623b461d Models know how much context they hold
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>
2026-08-01 00:33:50 +02:00

239 lines
9.2 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% 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 %}