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);
|
if (!locals.workspace || !locals.user) error(401);
|
||||||
|
|
||||||
const body = await request.json();
|
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`
|
const [row] = await sql`
|
||||||
UPDATE ai_model_aliases SET
|
UPDATE ai_model_aliases SET
|
||||||
|
alias = COALESCE(${body.alias ?? null}, alias),
|
||||||
description = COALESCE(${body.description ?? null}, description),
|
description = COALESCE(${body.description ?? null}, description),
|
||||||
is_active = COALESCE(${body.is_active ?? null}, is_active),
|
is_active = COALESCE(${body.is_active ?? null}, is_active),
|
||||||
updated_at = now()
|
updated_at = now()
|
||||||
|
|
|
||||||
|
|
@ -30,13 +30,14 @@ export const load: PageServerLoad = async () => {
|
||||||
const usage = await sql`
|
const usage = await sql`
|
||||||
SELECT
|
SELECT
|
||||||
model_alias,
|
model_alias,
|
||||||
|
model_actual,
|
||||||
count(*)::int AS call_count,
|
count(*)::int AS call_count,
|
||||||
sum(prompt_tokens)::int AS prompt_tokens,
|
sum(prompt_tokens)::int AS prompt_tokens,
|
||||||
sum(completion_tokens)::int AS completion_tokens,
|
sum(completion_tokens)::int AS completion_tokens,
|
||||||
sum(total_tokens)::int AS total_tokens
|
sum(total_tokens)::int AS total_tokens
|
||||||
FROM ai_usage_log
|
FROM ai_usage_log
|
||||||
WHERE created_at > now() - interval '30 days'
|
WHERE created_at > now() - interval '30 days'
|
||||||
GROUP BY model_alias
|
GROUP BY model_alias, model_actual
|
||||||
ORDER BY total_tokens DESC
|
ORDER BY total_tokens DESC
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@
|
||||||
|
|
||||||
interface UsageRow {
|
interface UsageRow {
|
||||||
model_alias: string;
|
model_alias: string;
|
||||||
|
model_actual: string | null;
|
||||||
call_count: number;
|
call_count: number;
|
||||||
prompt_tokens: number;
|
prompt_tokens: number;
|
||||||
completion_tokens: number;
|
completion_tokens: number;
|
||||||
|
|
@ -71,6 +72,11 @@
|
||||||
let editPromptText = $state('');
|
let editPromptText = $state('');
|
||||||
let expandedAlias = $state<string | null>(null);
|
let expandedAlias = $state<string | null>(null);
|
||||||
|
|
||||||
|
// Alias-redigering
|
||||||
|
let editingAlias = $state<string | null>(null);
|
||||||
|
let editAliasName = $state('');
|
||||||
|
let editAliasDesc = $state('');
|
||||||
|
|
||||||
// API-nøkler
|
// API-nøkler
|
||||||
let apiKeys = $state<ApiKey[]>([]);
|
let apiKeys = $state<ApiKey[]>([]);
|
||||||
let keysLoaded = $state(false);
|
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) {
|
async function toggleProvider(provider: Provider) {
|
||||||
saving = provider.id;
|
saving = provider.id;
|
||||||
errorMsg = '';
|
errorMsg = '';
|
||||||
|
|
@ -536,28 +600,46 @@
|
||||||
|
|
||||||
{#each aliases as alias (alias.id)}
|
{#each aliases as alias (alias.id)}
|
||||||
{@const ap = providersForAlias(alias.id)}
|
{@const ap = providersForAlias(alias.id)}
|
||||||
<div class="table-row" class:table-row--inactive={!alias.is_active}>
|
{#if editingAlias === alias.id}
|
||||||
<span
|
<div class="table-row alias-edit-row">
|
||||||
class="col-alias clickable"
|
<input type="text" class="alias-edit-input" bind:value={editAliasName} placeholder="Alias-navn" />
|
||||||
onclick={() => (expandedAlias = expandedAlias === alias.id ? null : alias.id)}
|
<input type="text" class="alias-edit-input" bind:value={editAliasDesc} placeholder="Beskrivelse" />
|
||||||
>
|
<span></span>
|
||||||
{alias.alias}
|
<span class="alias-edit-actions">
|
||||||
</span>
|
<button class="add-btn" onclick={() => saveAlias(alias)}>Lagre</button>
|
||||||
<span class="col-desc">{alias.description ?? '\u2014'}</span>
|
<button class="toggle-btn" onclick={cancelEditAlias}>Avbryt</button>
|
||||||
<span class="col-providers">{ap.length}</span>
|
</span>
|
||||||
<span class="col-active">
|
<span class="col-status">
|
||||||
<button class="toggle-btn" onclick={() => toggleAlias(alias)}>
|
{#if saving === alias.id}
|
||||||
{alias.is_active ? 'På' : 'Av'}
|
<span class="status-saving">...</span>
|
||||||
</button>
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
<span class="col-status">
|
</div>
|
||||||
{#if saving === alias.id}
|
{:else}
|
||||||
<span class="status-saving">...</span>
|
<div class="table-row" class:table-row--inactive={!alias.is_active}>
|
||||||
{:else if saved === alias.id}
|
<span
|
||||||
<span class="status-saved">OK</span>
|
class="col-alias clickable"
|
||||||
{/if}
|
onclick={() => (expandedAlias = expandedAlias === alias.id ? null : alias.id)}
|
||||||
</span>
|
>
|
||||||
</div>
|
{alias.alias}
|
||||||
|
</span>
|
||||||
|
<span class="col-desc">{alias.description ?? '\u2014'}</span>
|
||||||
|
<span class="col-providers">{ap.length}</span>
|
||||||
|
<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>
|
||||||
|
</span>
|
||||||
|
<span class="col-status">
|
||||||
|
{#if saving === alias.id}
|
||||||
|
<span class="status-saving">...</span>
|
||||||
|
{:else if saved === alias.id}
|
||||||
|
<span class="status-saved">OK</span>
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if expandedAlias === alias.id}
|
{#if expandedAlias === alias.id}
|
||||||
<div class="provider-list-alias">
|
<div class="provider-list-alias">
|
||||||
|
|
@ -739,29 +821,42 @@
|
||||||
|
|
||||||
<!-- Seksjon 5: Tokenforbruk -->
|
<!-- Seksjon 5: Tokenforbruk -->
|
||||||
<section>
|
<section>
|
||||||
<h3>Tokenforbruk (siste 30 dager)</h3>
|
<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}
|
{#if usage.length === 0}
|
||||||
<p class="hint">Ingen AI-kall registrert ennå.</p>
|
<p class="hint">Ingen AI-kall registrert ennå.</p>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="table-list">
|
<div class="table-list">
|
||||||
<div class="table-row table-row--header">
|
<div class="table-row table-row--header usage-row">
|
||||||
<span class="col-alias">Modellalias</span>
|
<span>Alias</span>
|
||||||
|
<span>Modell</span>
|
||||||
<span class="col-num">Kall</span>
|
<span class="col-num">Kall</span>
|
||||||
<span class="col-num">Prompt</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">Totalt</span>
|
||||||
|
<span class="col-num">Est. $</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#each usage as row}
|
{#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-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.call_count}</span>
|
||||||
<span class="col-num">{row.prompt_tokens.toLocaleString('nb-NO')}</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.completion_tokens.toLocaleString('nb-NO')}</span>
|
||||||
<span class="col-num">{row.total_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>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
{#if !catalogLoaded}
|
||||||
|
<p class="hint">Last inn modellkatalogen for å se estimerte kostnader.</p>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
@ -1040,6 +1135,27 @@
|
||||||
text-align: center;
|
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 {
|
.col-num {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
|
|
@ -1305,6 +1421,26 @@
|
||||||
font-style: italic;
|
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-seksjon */
|
||||||
.prompt-row {
|
.prompt-row {
|
||||||
grid-template-columns: 1.5fr 2.5fr 60px 90px 70px;
|
grid-template-columns: 1.5fr 2.5fr 60px 90px 70px;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue