feat: add provider and model routing tables, admin UI, and audit category for gateway
Implement the multi-provider routing system for the OpenAI gateway, including two new database tables (`gateway_providers`, `gateway_models`) ensured at init, a new admin page at `/admin/gateway` with full CRUD for providers and model routes, and a `"gateway"` audit category mapped to `"ai"`. The routing layer sits transparently on top of the existing single-provider default path: unmatched model names fall through unchanged, while matched routes forward to the configured provider with their own pricing economy, vision model, and context window. Cross-worker cache invalidation uses a shared `_ROUTING_CACHE` bumped via `"gateway_routing"` cache version.
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
{% 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"></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. 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"></div>
|
||||
<div class="admin-table-wrap">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr><th>Name</th><th>Base URL</th><th>Active</th><th class="gw-actions">Actions</th></tr>
|
||||
</thead>
|
||||
<tbody id="gw-providers"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<form class="gw-form" id="gw-provider-form" autocomplete="off">
|
||||
<p class="gw-form-title">Add or update provider</p>
|
||||
<div class="gw-field"><label>Name</label><input type="text" name="name" placeholder="openrouter" required></div>
|
||||
<div class="gw-field"><label>Base URL (chat completions)</label><input type="url" name="base_url" placeholder="https://openrouter.ai/api/v1/chat/completions"></div>
|
||||
<div class="gw-field"><label>API key</label><input type="password" name="api_key" placeholder="sk-..." autocomplete="new-password"></div>
|
||||
<div class="gw-field"><label>Active</label><select 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 (USD per 1M tokens). Chat uses cache-hit, cache-miss and output prices; embeddings use the input price; a vision model adds input and output pricing for the image description merge.</p>
|
||||
<div class="admin-table-wrap">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr><th>Source</th><th>Provider</th><th>Target</th><th>Kind</th><th>Vision</th><th class="gw-actions">Actions</th></tr>
|
||||
</thead>
|
||||
<tbody id="gw-models"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<form class="gw-form" id="gw-model-form" autocomplete="off">
|
||||
<p class="gw-form-title">Add or update model route</p>
|
||||
<div class="gw-field"><label>Source model (requested)</label><input type="text" name="source_model" placeholder="gpt-4o" required></div>
|
||||
<div class="gw-field"><label>Provider</label><select name="provider" data-provider-select></select></div>
|
||||
<div class="gw-field"><label>Target model (upstream)</label><input type="text" name="target_model" placeholder="openai/gpt-4o" required></div>
|
||||
<div class="gw-field"><label>Kind</label><select name="kind"><option value="chat">chat</option><option value="embed">embed</option></select></div>
|
||||
<div class="gw-field"><label>Vision provider</label><select name="vision_provider" data-provider-select></select></div>
|
||||
<div class="gw-field"><label>Vision model</label><input type="text" name="vision_model" placeholder="(optional) google/gemma-3-12b-it"></div>
|
||||
<div class="gw-field"><label>Context window</label><input type="number" name="context_window" min="0" value="0"></div>
|
||||
<div class="gw-field"><label>Active</label><select name="is_active"><option value="1">Yes</option><option value="0">No</option></select></div>
|
||||
<div class="gw-field"><label>Price cache-hit / 1M ($)</label><input type="number" name="price_cache_hit_per_m" min="0" step="0.0001" value="0"></div>
|
||||
<div class="gw-field"><label>Price cache-miss / 1M ($)</label><input type="number" name="price_cache_miss_per_m" min="0" step="0.0001" value="0"></div>
|
||||
<div class="gw-field"><label>Price output / 1M ($)</label><input type="number" name="price_output_per_m" min="0" step="0.0001" value="0"></div>
|
||||
<div class="gw-field"><label>Price input / 1M ($) (embed/vision)</label><input type="number" name="price_input_per_m" min="0" step="0.0001" 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 %}
|
||||
Reference in New Issue
Block a user