88 lines
9.2 KiB
HTML
88 lines
9.2 KiB
HTML
{% extends "admin_base.html" %}
|
|
{% block extra_head %}
|
|
{{ super() }}
|
|
<link rel="stylesheet" href="{{ static_url('/static/css/gateway.css') }}">
|
|
{% endblock %}
|
|
{% block admin_content %}
|
|
<div id="gateway-admin">
|
|
<div class="admin-toolbar">
|
|
<h2>Gateway routing</h2>
|
|
<span class="admin-count" id="gw-count" role="status" aria-live="polite"></span>
|
|
</div>
|
|
<p class="gw-intro">Map any requested model name onto a provider and target model, each with its own pricing economy and an optional vision model for image to text merging. Image routes use a flat per-image price. Unmapped requests fall through to the default upstream unchanged.</p>
|
|
|
|
<section class="gw-section">
|
|
<div class="gw-section-head">
|
|
<h3>Providers</h3>
|
|
</div>
|
|
<p class="gw-section-hint">Named upstreams reused across model routes. A model route with a blank provider uses the default below.</p>
|
|
<div class="gw-default" id="gw-default" role="status" aria-live="polite"></div>
|
|
<div class="admin-table-wrap">
|
|
<table class="admin-table">
|
|
<caption class="sr-only">Providers</caption>
|
|
<thead>
|
|
<tr><th scope="col">Name</th><th scope="col">Base URL</th><th scope="col">Active</th><th scope="col" class="gw-actions">Actions</th></tr>
|
|
</thead>
|
|
<tbody id="gw-providers"></tbody>
|
|
</table>
|
|
</div>
|
|
<form class="gw-form" id="gw-provider-form" autocomplete="off" role="group" aria-label="Add or update provider">
|
|
<p class="gw-form-title">Add or update provider</p>
|
|
<div class="gw-field"><label for="gw-provider-name">Name</label><input type="text" id="gw-provider-name" name="name" placeholder="openrouter" required aria-required="true"></div>
|
|
<div class="gw-field"><label for="gw-provider-base-url">Base URL (chat completions)</label><input type="url" id="gw-provider-base-url" name="base_url" placeholder="https://openrouter.ai/api/v1/chat/completions"></div>
|
|
<div class="gw-field"><label for="gw-provider-api-key">API key</label><input type="password" id="gw-provider-api-key" name="api_key" placeholder="sk-..." autocomplete="new-password"></div>
|
|
<div class="gw-field"><label for="gw-provider-active">Active</label><select id="gw-provider-active" name="is_active"><option value="1">Yes</option><option value="0">No</option></select></div>
|
|
<div class="gw-form-actions"><button type="submit" class="admin-btn admin-btn-primary">Save provider</button></div>
|
|
</form>
|
|
</section>
|
|
|
|
<section class="gw-section">
|
|
<div class="gw-section-head">
|
|
<h3>Model routes</h3>
|
|
</div>
|
|
<p class="gw-section-hint">Each source model maps to a provider and target model with its own economy. Chat uses cache-hit, cache-miss and output prices (USD per 1M tokens); embeddings use the input price; image routes use the input price as a flat USD per image; a vision model adds input and output pricing for the image description merge.</p>
|
|
<div class="admin-table-wrap">
|
|
<table class="admin-table">
|
|
<caption class="sr-only">Model routes</caption>
|
|
<thead>
|
|
<tr><th scope="col">Source</th><th scope="col">Provider</th><th scope="col">Target</th><th scope="col">Kind</th><th scope="col">Vision</th><th scope="col">Economy</th><th scope="col" class="gw-actions">Actions</th></tr>
|
|
</thead>
|
|
<tbody id="gw-models"></tbody>
|
|
</table>
|
|
</div>
|
|
<form class="gw-form" id="gw-model-form" autocomplete="off" role="group" aria-label="Add or update model route">
|
|
<p class="gw-form-title">Add or update model route</p>
|
|
<div class="gw-field"><label for="gw-model-source">Source model (requested)</label><input type="text" id="gw-model-source" name="source_model" placeholder="gpt-4o" required aria-required="true"></div>
|
|
<div class="gw-field"><label for="gw-model-provider">Provider</label><select id="gw-model-provider" name="provider" data-provider-select></select></div>
|
|
<div class="gw-field"><label for="gw-model-target">Target model (upstream)</label><input type="text" id="gw-model-target" name="target_model" placeholder="openai/gpt-4o" required aria-required="true"></div>
|
|
<div class="gw-field"><label for="gw-model-kind">Kind</label><select id="gw-model-kind" name="kind"><option value="chat">chat</option><option value="embed">embed</option><option value="image">image</option></select></div>
|
|
<div class="gw-field" data-kind-field="chat"><label for="gw-model-vision-provider">Vision provider</label><select id="gw-model-vision-provider" name="vision_provider" data-provider-select></select></div>
|
|
<div class="gw-field" data-kind-field="chat"><label for="gw-model-vision-model">Vision model</label><input type="text" id="gw-model-vision-model" name="vision_model" placeholder="(optional) google/gemma-3-12b-it"></div>
|
|
<div class="gw-field"><label for="gw-model-context">Context window</label><input type="number" id="gw-model-context" name="context_window" min="0" value="0"></div>
|
|
<div class="gw-field"><label for="gw-model-active">Active</label><select id="gw-model-active" name="is_active"><option value="1">Yes</option><option value="0">No</option></select></div>
|
|
<div class="gw-field" data-kind-field="chat"><label for="gw-model-price-cache-hit">Price cache-hit / 1M ($)</label><input type="number" id="gw-model-price-cache-hit" name="price_cache_hit_per_m" min="0" step="0.0001" value="0"></div>
|
|
<div class="gw-field" data-kind-field="chat"><label for="gw-model-price-cache-miss">Price cache-miss / 1M ($)</label><input type="number" id="gw-model-price-cache-miss" name="price_cache_miss_per_m" min="0" step="0.0001" value="0"></div>
|
|
<div class="gw-field" data-kind-field="chat"><label for="gw-model-price-output">Price output / 1M ($)</label><input type="number" id="gw-model-price-output" name="price_output_per_m" min="0" step="0.0001" value="0"></div>
|
|
<div class="gw-field" data-kind-field="embed chat"><label for="gw-model-price-input" id="gw-model-price-input-label">Price input / 1M ($) (embed/vision)</label><input type="number" id="gw-model-price-input" name="price_input_per_m" min="0" step="0.0001" value="0"></div>
|
|
<p class="gw-form-title gw-form-subtitle" data-kind-field="chat embed">Tiered / off-peak pricing (optional)</p>
|
|
<p class="gw-section-hint" data-kind-field="chat embed">Some providers charge a different rate once a request crosses a context-length threshold, or discount a fixed time-of-day window. Leave blank/zero to keep the flat rates above at all times.</p>
|
|
<div class="gw-field" data-kind-field="chat embed"><label for="gw-model-tier-threshold">Tier-2 threshold (input tokens)</label><input type="number" id="gw-model-tier-threshold" name="context_tier_threshold_tokens" min="0" value="0" title="0 disables tiered pricing"></div>
|
|
<div class="gw-field" data-kind-field="chat"><label for="gw-model-price-cache-hit-tier2">Tier-2 price cache-hit / 1M ($)</label><input type="number" id="gw-model-price-cache-hit-tier2" name="price_cache_hit_per_m_tier2" min="0" step="0.0001" placeholder="same as tier 1"></div>
|
|
<div class="gw-field" data-kind-field="chat"><label for="gw-model-price-cache-miss-tier2">Tier-2 price cache-miss / 1M ($)</label><input type="number" id="gw-model-price-cache-miss-tier2" name="price_cache_miss_per_m_tier2" min="0" step="0.0001" placeholder="same as tier 1"></div>
|
|
<div class="gw-field" data-kind-field="chat"><label for="gw-model-price-output-tier2">Tier-2 price output / 1M ($)</label><input type="number" id="gw-model-price-output-tier2" name="price_output_per_m_tier2" min="0" step="0.0001" placeholder="same as tier 1"></div>
|
|
<div class="gw-field" data-kind-field="chat embed"><label for="gw-model-price-input-tier2">Tier-2 price input / 1M ($) (embed/vision)</label><input type="number" id="gw-model-price-input-tier2" name="price_input_per_m_tier2" min="0" step="0.0001" placeholder="same as tier 1"></div>
|
|
<div class="gw-field" data-kind-field="chat embed image"><label for="gw-model-off-peak-start">Off-peak start (UTC)</label><input type="time" id="gw-model-off-peak-start" name="off_peak_start" title="Leave blank to disable off-peak discount"></div>
|
|
<div class="gw-field" data-kind-field="chat embed image"><label for="gw-model-off-peak-end">Off-peak end (UTC)</label><input type="time" id="gw-model-off-peak-end" name="off_peak_end" title="Leave blank to disable off-peak discount"></div>
|
|
<div class="gw-field" data-kind-field="chat embed image"><label for="gw-model-off-peak-discount">Off-peak discount (%)</label><input type="number" id="gw-model-off-peak-discount" name="off_peak_discount_pct" min="0" max="100" step="0.1" value="0"></div>
|
|
<div class="gw-form-actions"><button type="submit" class="admin-btn admin-btn-primary">Save model route</button></div>
|
|
</form>
|
|
</section>
|
|
</div>
|
|
{% endblock %}
|
|
{% block extra_js %}
|
|
<script type="module">
|
|
import { GatewayAdmin } from "{{ static_url('/static/js/GatewayAdmin.js') }}";
|
|
new GatewayAdmin(document.getElementById("gateway-admin")).start();
|
|
</script>
|
|
{% endblock %}
|