AI-admin: alias-rename, dollarkostnad, fikset kolonnebredder
- PATCH /aliases/:id støtter nå rename (alias-felt) - Alias-raden har «Rediger»-knapp → inline-redigering av navn + beskrivelse - Tokenforbruk viser model_actual og estimert dollarkostnad per rad - Dollarkostnad beregnes fra OpenRouter-katalogpriser (krever lastet katalog) - Tokenforbruk-tabellen bruker auto-kolonnebredde (fikser overflow) - «Kompl.» i stedet for «Completion» i header Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
177e4b6b66
commit
21683bd660
3 changed files with 1512 additions and 1377 deletions
|
|
@ -7,12 +7,10 @@ export const PATCH: RequestHandler = async ({ params, request, locals }) => {
|
|||
if (!locals.workspace || !locals.user) error(401);
|
||||
|
||||
const body = await request.json();
|
||||
const updates: Record<string, unknown> = {};
|
||||
if ('description' in body) updates.description = body.description;
|
||||
if ('is_active' in body) updates.is_active = body.is_active;
|
||||
|
||||
const [row] = await sql`
|
||||
UPDATE ai_model_aliases SET
|
||||
alias = COALESCE(${body.alias ?? null}, alias),
|
||||
description = COALESCE(${body.description ?? null}, description),
|
||||
is_active = COALESCE(${body.is_active ?? null}, is_active),
|
||||
updated_at = now()
|
||||
|
|
|
|||
|
|
@ -30,13 +30,14 @@ export const load: PageServerLoad = async () => {
|
|||
const usage = await sql`
|
||||
SELECT
|
||||
model_alias,
|
||||
model_actual,
|
||||
count(*)::int AS call_count,
|
||||
sum(prompt_tokens)::int AS prompt_tokens,
|
||||
sum(completion_tokens)::int AS completion_tokens,
|
||||
sum(total_tokens)::int AS total_tokens
|
||||
FROM ai_usage_log
|
||||
WHERE created_at > now() - interval '30 days'
|
||||
GROUP BY model_alias
|
||||
GROUP BY model_alias, model_actual
|
||||
ORDER BY total_tokens DESC
|
||||
`;
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@
|
|||
|
||||
interface UsageRow {
|
||||
model_alias: string;
|
||||
model_actual: string | null;
|
||||
call_count: number;
|
||||
prompt_tokens: number;
|
||||
completion_tokens: number;
|
||||
|
|
@ -71,6 +72,11 @@
|
|||
let editPromptText = $state('');
|
||||
let expandedAlias = $state<string | null>(null);
|
||||
|
||||
// Alias-redigering
|
||||
let editingAlias = $state<string | null>(null);
|
||||
let editAliasName = $state('');
|
||||
let editAliasDesc = $state('');
|
||||
|
||||
// API-nøkler
|
||||
let apiKeys = $state<ApiKey[]>([]);
|
||||
let keysLoaded = $state(false);
|
||||
|
|
@ -250,6 +256,64 @@
|
|||
}
|
||||
}
|
||||
|
||||
function startEditAlias(alias: Alias) {
|
||||
editingAlias = alias.id;
|
||||
editAliasName = alias.alias;
|
||||
editAliasDesc = alias.description ?? '';
|
||||
}
|
||||
|
||||
function cancelEditAlias() {
|
||||
editingAlias = null;
|
||||
editAliasName = '';
|
||||
editAliasDesc = '';
|
||||
}
|
||||
|
||||
async function saveAlias(alias: Alias) {
|
||||
saving = alias.id;
|
||||
errorMsg = '';
|
||||
try {
|
||||
const res = await fetch(`/api/admin/ai/aliases/${alias.id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ alias: editAliasName, description: editAliasDesc || null })
|
||||
});
|
||||
if (!res.ok) throw new Error('Feil ved lagring');
|
||||
const updated = await res.json();
|
||||
alias.alias = updated.alias;
|
||||
alias.description = updated.description;
|
||||
editingAlias = null;
|
||||
markSaved(alias.id);
|
||||
} catch {
|
||||
errorMsg = 'Kunne ikke oppdatere alias';
|
||||
} finally {
|
||||
saving = null;
|
||||
}
|
||||
}
|
||||
|
||||
function estimateCost(row: UsageRow): number | null {
|
||||
if (!catalogLoaded || !row.model_actual) return null;
|
||||
const model = catalogModels.find((m) => m.id === row.model_actual);
|
||||
if (!model) return null;
|
||||
return (
|
||||
(row.prompt_tokens / 1_000_000) * model.prompt_price_per_m +
|
||||
(row.completion_tokens / 1_000_000) * model.completion_price_per_m
|
||||
);
|
||||
}
|
||||
|
||||
let totalEstimatedCost = $derived.by(() => {
|
||||
if (!catalogLoaded) return null;
|
||||
let total = 0;
|
||||
let hasAny = false;
|
||||
for (const row of usage) {
|
||||
const cost = estimateCost(row);
|
||||
if (cost !== null) {
|
||||
total += cost;
|
||||
hasAny = true;
|
||||
}
|
||||
}
|
||||
return hasAny ? total : null;
|
||||
});
|
||||
|
||||
async function toggleProvider(provider: Provider) {
|
||||
saving = provider.id;
|
||||
errorMsg = '';
|
||||
|
|
@ -536,6 +600,22 @@
|
|||
|
||||
{#each aliases as alias (alias.id)}
|
||||
{@const ap = providersForAlias(alias.id)}
|
||||
{#if editingAlias === alias.id}
|
||||
<div class="table-row alias-edit-row">
|
||||
<input type="text" class="alias-edit-input" bind:value={editAliasName} placeholder="Alias-navn" />
|
||||
<input type="text" class="alias-edit-input" bind:value={editAliasDesc} placeholder="Beskrivelse" />
|
||||
<span></span>
|
||||
<span class="alias-edit-actions">
|
||||
<button class="add-btn" onclick={() => saveAlias(alias)}>Lagre</button>
|
||||
<button class="toggle-btn" onclick={cancelEditAlias}>Avbryt</button>
|
||||
</span>
|
||||
<span class="col-status">
|
||||
{#if saving === alias.id}
|
||||
<span class="status-saving">...</span>
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="table-row" class:table-row--inactive={!alias.is_active}>
|
||||
<span
|
||||
class="col-alias clickable"
|
||||
|
|
@ -545,7 +625,8 @@
|
|||
</span>
|
||||
<span class="col-desc">{alias.description ?? '\u2014'}</span>
|
||||
<span class="col-providers">{ap.length}</span>
|
||||
<span class="col-active">
|
||||
<span class="col-active alias-action-group">
|
||||
<button class="toggle-btn" onclick={() => startEditAlias(alias)}>Rediger</button>
|
||||
<button class="toggle-btn" onclick={() => toggleAlias(alias)}>
|
||||
{alias.is_active ? 'På' : 'Av'}
|
||||
</button>
|
||||
|
|
@ -558,6 +639,7 @@
|
|||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if expandedAlias === alias.id}
|
||||
<div class="provider-list-alias">
|
||||
|
|
@ -739,29 +821,42 @@
|
|||
|
||||
<!-- Seksjon 5: Tokenforbruk -->
|
||||
<section>
|
||||
<div class="catalog-header">
|
||||
<h3>Tokenforbruk (siste 30 dager)</h3>
|
||||
{#if totalEstimatedCost !== null}
|
||||
<span class="usage-total-cost">Estimert totalkostnad: ${totalEstimatedCost.toFixed(2)}</span>
|
||||
{/if}
|
||||
</div>
|
||||
{#if usage.length === 0}
|
||||
<p class="hint">Ingen AI-kall registrert ennå.</p>
|
||||
{:else}
|
||||
<div class="table-list">
|
||||
<div class="table-row table-row--header">
|
||||
<span class="col-alias">Modellalias</span>
|
||||
<div class="table-row table-row--header usage-row">
|
||||
<span>Alias</span>
|
||||
<span>Modell</span>
|
||||
<span class="col-num">Kall</span>
|
||||
<span class="col-num">Prompt</span>
|
||||
<span class="col-num">Completion</span>
|
||||
<span class="col-num">Kompl.</span>
|
||||
<span class="col-num">Totalt</span>
|
||||
<span class="col-num">Est. $</span>
|
||||
</div>
|
||||
|
||||
{#each usage as row}
|
||||
<div class="table-row">
|
||||
{@const cost = estimateCost(row)}
|
||||
<div class="table-row usage-row">
|
||||
<span class="col-alias">{row.model_alias}</span>
|
||||
<span class="col-model-actual">{row.model_actual ?? '\u2014'}</span>
|
||||
<span class="col-num">{row.call_count}</span>
|
||||
<span class="col-num">{row.prompt_tokens.toLocaleString('nb-NO')}</span>
|
||||
<span class="col-num">{row.completion_tokens.toLocaleString('nb-NO')}</span>
|
||||
<span class="col-num">{row.total_tokens.toLocaleString('nb-NO')}</span>
|
||||
<span class="col-num">{cost !== null ? `$${cost.toFixed(2)}` : '\u2014'}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{#if !catalogLoaded}
|
||||
<p class="hint">Last inn modellkatalogen for å se estimerte kostnader.</p>
|
||||
{/if}
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
|
|
@ -1040,6 +1135,27 @@
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
/* Alias edit */
|
||||
.alias-edit-row {
|
||||
grid-template-columns: 2fr 2fr 1fr auto 40px;
|
||||
}
|
||||
|
||||
.alias-edit-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.alias-edit-actions {
|
||||
display: flex;
|
||||
gap: 0.35rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.alias-action-group {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.col-num {
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
|
|
@ -1305,6 +1421,26 @@
|
|||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Usage/tokenforbruk */
|
||||
.usage-row {
|
||||
grid-template-columns: auto auto auto auto auto auto auto;
|
||||
}
|
||||
|
||||
.col-model-actual {
|
||||
font-family: monospace;
|
||||
font-size: 0.7rem;
|
||||
color: #8b92a5;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.usage-total-cost {
|
||||
font-size: 0.85rem;
|
||||
color: #4ade80;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Prompt-seksjon */
|
||||
.prompt-row {
|
||||
grid-template-columns: 1.5fr 2.5fr 60px 90px 70px;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue