AI-admin: opp/ned-knapper for å endre provider-prioritet per alias

- Nytt swap-endpoint (POST /api/admin/ai/providers/swap) for atomisk bytte
- Pil opp/ned ved hver provider-rad for å endre fallback-rekkefølge
- Prioritet avgjør hvilken modell LiteLLM prøver først per alias

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
vegard 2026-03-16 06:55:55 +01:00
parent b082edc2bd
commit 8652f0969f
2 changed files with 118 additions and 3 deletions

View file

@ -0,0 +1,46 @@
import { json, error } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { sql } from '$lib/server/db';
/** POST — bytt prioritet mellom to providers (atomisk) */
export const POST: RequestHandler = async ({ request, locals }) => {
if (!locals.workspace || !locals.user) error(401);
const { id_a, id_b } = await request.json();
if (!id_a || !id_b) error(400, 'id_a og id_b kreves');
// Atomisk swap via CTE — unngår UNIQUE-constraint-brudd
const rows = await sql`
WITH swap AS (
SELECT
a.id AS id_a, a.priority AS pri_a,
b.id AS id_b, b.priority AS pri_b
FROM ai_model_providers a, ai_model_providers b
WHERE a.id = ${id_a}::uuid AND b.id = ${id_b}::uuid
AND a.alias_id = b.alias_id
),
update_a AS (
UPDATE ai_model_providers SET priority = -1, updated_at = now()
WHERE id = (SELECT id_a FROM swap)
),
update_b AS (
UPDATE ai_model_providers SET priority = (SELECT pri_a FROM swap), updated_at = now()
WHERE id = (SELECT id_b FROM swap)
)
UPDATE ai_model_providers SET priority = (SELECT pri_b FROM swap), updated_at = now()
WHERE id = (SELECT id_a FROM swap)
RETURNING id
`;
if (rows.length === 0) error(400, 'Kunne ikke bytte — sjekk at begge tilhører samme alias');
// Returner oppdaterte providers for aliaset
const updated = await sql`
SELECT id, alias_id, priority, litellm_model, api_key_env, is_active, extra_params
FROM ai_model_providers
WHERE alias_id = (SELECT alias_id FROM ai_model_providers WHERE id = ${id_a}::uuid)
ORDER BY priority ASC
`;
return json(updated);
};

View file

@ -479,6 +479,31 @@
} }
} }
async function swapProviders(providerA: Provider, providerB: Provider) {
saving = providerA.id;
errorMsg = '';
try {
const res = await fetch('/api/admin/ai/providers/swap', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id_a: providerA.id, id_b: providerB.id })
});
if (!res.ok) throw new Error('Feil ved bytte');
const updated: Provider[] = await res.json();
// Oppdater providers-listen med nye prioriteter
for (const u of updated) {
const existing = providers.find(p => p.id === u.id);
if (existing) existing.priority = u.priority;
}
providers = [...providers];
markSaved(providerA.id);
} catch {
errorMsg = 'Kunne ikke bytte rekkefølge';
} finally {
saving = null;
}
}
async function toggleProvider(provider: Provider) { async function toggleProvider(provider: Provider) {
saving = provider.id; saving = provider.id;
errorMsg = ''; errorMsg = '';
@ -894,10 +919,26 @@
{#if expandedAlias === alias.id} {#if expandedAlias === alias.id}
<div class="provider-list-alias"> <div class="provider-list-alias">
{#each ap as provider (provider.id)} {#each ap as provider, idx (provider.id)}
{@const keyDisabled = !isKeyEnabled(provider.api_key_env)} {@const keyDisabled = !isKeyEnabled(provider.api_key_env)}
<div class="provider-row-alias" class:provider-row--inactive={!provider.is_active} class:provider-row--key-off={keyDisabled}> <div class="provider-row-alias" class:provider-row--inactive={!provider.is_active} class:provider-row--key-off={keyDisabled}>
<span class="col-pri">#{provider.priority}</span> <span class="col-pri">
<span class="pri-arrows">
<button
class="pri-btn"
disabled={idx === 0}
onclick={() => swapProviders(provider, ap[idx - 1])}
title="Flytt opp"
>&#x25B2;</button>
<button
class="pri-btn"
disabled={idx === ap.length - 1}
onclick={() => swapProviders(provider, ap[idx + 1])}
title="Flytt ned"
>&#x25BC;</button>
</span>
#{provider.priority}
</span>
<span class="col-model">{provider.litellm_model}</span> <span class="col-model">{provider.litellm_model}</span>
<span class="col-key" class:col-key--disabled={keyDisabled}>{provider.api_key_env}{#if keyDisabled} (av){/if}</span> <span class="col-key" class:col-key--disabled={keyDisabled}>{provider.api_key_env}{#if keyDisabled} (av){/if}</span>
<span class="col-extra-pills"> <span class="col-extra-pills">
@ -1560,7 +1601,35 @@
.col-pri { .col-pri {
color: #8b92a5; color: #8b92a5;
font-size: 0.75rem; font-size: 0.75rem;
width: 30px; width: 60px;
display: flex;
align-items: center;
gap: 0.25rem;
}
.pri-arrows {
display: flex;
flex-direction: column;
gap: 0;
}
.pri-btn {
background: none;
border: none;
color: #6c7086;
font-size: 0.55rem;
cursor: pointer;
padding: 0;
line-height: 1;
}
.pri-btn:hover:not(:disabled) {
color: #cdd6f4;
}
.pri-btn:disabled {
opacity: 0.2;
cursor: default;
} }
.col-model { .col-model {