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:
parent
b082edc2bd
commit
8652f0969f
2 changed files with 118 additions and 3 deletions
46
web/src/routes/api/admin/ai/providers/swap/+server.ts
Normal file
46
web/src/routes/api/admin/ai/providers/swap/+server.ts
Normal 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);
|
||||
};
|
||||
|
|
@ -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) {
|
||||
saving = provider.id;
|
||||
errorMsg = '';
|
||||
|
|
@ -894,10 +919,26 @@
|
|||
|
||||
{#if expandedAlias === alias.id}
|
||||
<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)}
|
||||
<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"
|
||||
>▲</button>
|
||||
<button
|
||||
class="pri-btn"
|
||||
disabled={idx === ap.length - 1}
|
||||
onclick={() => swapProviders(provider, ap[idx + 1])}
|
||||
title="Flytt ned"
|
||||
>▼</button>
|
||||
</span>
|
||||
#{provider.priority}
|
||||
</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-extra-pills">
|
||||
|
|
@ -1560,7 +1601,35 @@
|
|||
.col-pri {
|
||||
color: #8b92a5;
|
||||
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 {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue