Update
This commit is contained in:
@@ -15,9 +15,25 @@ export class GatewayAdmin {
|
||||
|
||||
async start() {
|
||||
this.bind();
|
||||
this.syncKindFields();
|
||||
await this.reload();
|
||||
}
|
||||
|
||||
syncKindFields() {
|
||||
const kind = this.modelForm.kind.value || "chat";
|
||||
const priceInputLabel = this.root.querySelector("#gw-model-price-input-label");
|
||||
if (priceInputLabel) {
|
||||
priceInputLabel.textContent =
|
||||
kind === "image"
|
||||
? "Price per image ($)"
|
||||
: "Price input / 1M ($) (embed/vision)";
|
||||
}
|
||||
this.root.querySelectorAll("[data-kind-field]").forEach((field) => {
|
||||
const kinds = (field.dataset.kindField || "").split(/\s+/).filter(Boolean);
|
||||
field.style.display = kinds.includes(kind) ? "" : "none";
|
||||
});
|
||||
}
|
||||
|
||||
bind() {
|
||||
this.providerForm.addEventListener("submit", (event) => {
|
||||
event.preventDefault();
|
||||
@@ -27,6 +43,7 @@ export class GatewayAdmin {
|
||||
event.preventDefault();
|
||||
this.saveModel();
|
||||
});
|
||||
this.modelForm.kind.addEventListener("change", () => this.syncKindFields());
|
||||
this.providersBody.addEventListener("click", (event) => this.onProviderClick(event));
|
||||
this.modelsBody.addEventListener("click", (event) => this.onModelClick(event));
|
||||
}
|
||||
@@ -71,7 +88,9 @@ export class GatewayAdmin {
|
||||
el.innerHTML = `
|
||||
<strong>default</strong> (from <a href="/admin/services">Services config</a>):
|
||||
chat <code class="gw-code">${this.escape(def.model)}</code> at <code class="gw-code">${this.escape(def.base_url)}</code>,
|
||||
embed <code class="gw-code">${this.escape(def.embed_model)}</code>, vision <code class="gw-code">${this.escape(def.vision_model)}</code>`;
|
||||
embed <code class="gw-code">${this.escape(def.embed_model)}</code>,
|
||||
image <code class="gw-code">${this.escape(def.image_model)}</code>,
|
||||
vision <code class="gw-code">${this.escape(def.vision_model)}</code>`;
|
||||
}
|
||||
|
||||
renderProviders() {
|
||||
@@ -109,7 +128,7 @@ export class GatewayAdmin {
|
||||
const data = await Http.getJson("/admin/gateway/models");
|
||||
const models = data.models || [];
|
||||
if (!models.length) {
|
||||
this.modelsBody.innerHTML = `<tr><td colspan="6" class="admin-empty">No model routes. Requests fall through to the default upstream.</td></tr>`;
|
||||
this.modelsBody.innerHTML = `<tr><td colspan="7" class="admin-empty">No model routes. Requests fall through to the default upstream.</td></tr>`;
|
||||
return 0;
|
||||
}
|
||||
this.modelsBody.innerHTML = models
|
||||
@@ -118,12 +137,26 @@ export class GatewayAdmin {
|
||||
? `<code class="gw-code">${this.escape(m.vision_provider || m.provider || "default")}/${this.escape(m.vision_model)}</code>`
|
||||
: `<span class="gw-muted">-</span>`;
|
||||
const provider = m.provider || "default";
|
||||
const badges = [];
|
||||
if (m.context_tier_threshold_tokens) {
|
||||
badges.push(`<span class="gw-badge" title="Tier-2 rates above ${m.context_tier_threshold_tokens} input tokens">tiered</span>`);
|
||||
}
|
||||
if (m.off_peak_start_minute !== null && m.off_peak_start_minute !== undefined) {
|
||||
const start = this.minutesToTime(m.off_peak_start_minute);
|
||||
const end = this.minutesToTime(m.off_peak_end_minute);
|
||||
badges.push(`<span class="gw-badge" title="${m.off_peak_discount_pct}% off ${start}-${end} UTC">off-peak</span>`);
|
||||
}
|
||||
if (m.kind === "image" && m.price_input_per_m) {
|
||||
badges.push(`<span class="gw-badge" title="Flat price per generated image">$${m.price_input_per_m}/img</span>`);
|
||||
}
|
||||
const economy = badges.length ? badges.join(" ") : `<span class="gw-muted">-</span>`;
|
||||
return `<tr>
|
||||
<td>${this.escape(m.source_model)}</td>
|
||||
<td>${this.escape(provider)}</td>
|
||||
<td><code class="gw-code">${this.escape(m.target_model)}</code></td>
|
||||
<td>${this.escape(m.kind)}</td>
|
||||
<td>${vision}</td>
|
||||
<td>${economy}</td>
|
||||
<td class="gw-actions">
|
||||
<button class="admin-btn admin-btn-sm" data-edit-model='${this.attr(JSON.stringify(m))}'>Edit</button>
|
||||
<button class="admin-btn admin-btn-sm admin-btn-danger" data-del-model="${this.attr(m.source_model)}">Delete</button>
|
||||
@@ -142,6 +175,28 @@ export class GatewayAdmin {
|
||||
return values;
|
||||
}
|
||||
|
||||
timeToMinutes(value) {
|
||||
if (!value) return null;
|
||||
const [hours, minutes] = value.split(":").map((part) => parseInt(part, 10));
|
||||
if (Number.isNaN(hours) || Number.isNaN(minutes)) return null;
|
||||
return hours * 60 + minutes;
|
||||
}
|
||||
|
||||
minutesToTime(minutes) {
|
||||
if (minutes === null || minutes === undefined || minutes === "") return "";
|
||||
const total = parseInt(minutes, 10);
|
||||
if (Number.isNaN(total)) return "";
|
||||
const hours = Math.floor(total / 60) % 24;
|
||||
const mins = total % 60;
|
||||
return `${String(hours).padStart(2, "0")}:${String(mins).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
optionalFloat(value) {
|
||||
if (value === "" || value === null || value === undefined) return null;
|
||||
const parsed = parseFloat(value);
|
||||
return Number.isNaN(parsed) ? null : parsed;
|
||||
}
|
||||
|
||||
async saveProvider() {
|
||||
const values = this.formValues(this.providerForm);
|
||||
const payload = {
|
||||
@@ -174,6 +229,14 @@ export class GatewayAdmin {
|
||||
price_cache_miss_per_m: parseFloat(values.price_cache_miss_per_m) || 0,
|
||||
price_output_per_m: parseFloat(values.price_output_per_m) || 0,
|
||||
price_input_per_m: parseFloat(values.price_input_per_m) || 0,
|
||||
context_tier_threshold_tokens: parseInt(values.context_tier_threshold_tokens, 10) || 0,
|
||||
price_cache_hit_per_m_tier2: this.optionalFloat(values.price_cache_hit_per_m_tier2),
|
||||
price_cache_miss_per_m_tier2: this.optionalFloat(values.price_cache_miss_per_m_tier2),
|
||||
price_output_per_m_tier2: this.optionalFloat(values.price_output_per_m_tier2),
|
||||
price_input_per_m_tier2: this.optionalFloat(values.price_input_per_m_tier2),
|
||||
off_peak_start_minute: this.timeToMinutes(values.off_peak_start),
|
||||
off_peak_end_minute: this.timeToMinutes(values.off_peak_end),
|
||||
off_peak_discount_pct: parseFloat(values.off_peak_discount_pct) || 0,
|
||||
is_active: values.is_active === "1",
|
||||
};
|
||||
try {
|
||||
@@ -243,6 +306,7 @@ export class GatewayAdmin {
|
||||
form.provider.value = model.provider || "";
|
||||
form.target_model.value = model.target_model || "";
|
||||
form.kind.value = model.kind || "chat";
|
||||
this.syncKindFields();
|
||||
form.vision_provider.value = model.vision_provider || "";
|
||||
form.vision_model.value = model.vision_model || "";
|
||||
form.context_window.value = model.context_window || 0;
|
||||
@@ -250,6 +314,14 @@ export class GatewayAdmin {
|
||||
form.price_cache_miss_per_m.value = model.price_cache_miss_per_m || 0;
|
||||
form.price_output_per_m.value = model.price_output_per_m || 0;
|
||||
form.price_input_per_m.value = model.price_input_per_m || 0;
|
||||
form.context_tier_threshold_tokens.value = model.context_tier_threshold_tokens || 0;
|
||||
form.price_cache_hit_per_m_tier2.value = model.price_cache_hit_per_m_tier2 ?? "";
|
||||
form.price_cache_miss_per_m_tier2.value = model.price_cache_miss_per_m_tier2 ?? "";
|
||||
form.price_output_per_m_tier2.value = model.price_output_per_m_tier2 ?? "";
|
||||
form.price_input_per_m_tier2.value = model.price_input_per_m_tier2 ?? "";
|
||||
form.off_peak_start.value = this.minutesToTime(model.off_peak_start_minute);
|
||||
form.off_peak_end.value = this.minutesToTime(model.off_peak_end_minute);
|
||||
form.off_peak_discount_pct.value = model.off_peak_discount_pct || 0;
|
||||
form.is_active.value = model.is_active ? "1" : "0";
|
||||
form.source_model.scrollIntoView({ block: "center" });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user