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) {
|
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"
|
||||||
|
>▲</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-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 {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue