AI-admin: extra_params per provider (web-plugin, custom params)
- Ny kolonne extra_params JSONB på ai_model_providers (migrasjon 0009)
- Web-søk toggle-pill per provider — ett klikk for å slå på/av
- «...»-knapp åpner JSON-editor for vilkårlige extra_params
- Config-generering fletter extra_params inn i litellm_params
- POST/PATCH provider-endepunkter støtter extra_params
Eksempel: Grok med web-plugin genererer:
plugins: [{"id":"web"}]
under litellm_params i config.yaml.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
ad0d4f1f8a
commit
35c76a7038
6 changed files with 202 additions and 8 deletions
13
migrations/0009_provider_extra_params.sql
Normal file
13
migrations/0009_provider_extra_params.sql
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
-- 0009_provider_extra_params.sql
|
||||
-- Legger til extra_params JSONB på ai_model_providers for leverandør-spesifikke
|
||||
-- parametere (f.eks. OpenRouter web-plugin, domenefilter).
|
||||
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE ai_model_providers
|
||||
ADD COLUMN extra_params JSONB;
|
||||
|
||||
COMMENT ON COLUMN ai_model_providers.extra_params IS
|
||||
'Valgfrie leverandør-spesifikke params som flettes inn i litellm_params ved config-generering. Eksempel: {"plugins": [{"id": "web"}]}';
|
||||
|
||||
COMMIT;
|
||||
|
|
@ -17,7 +17,8 @@ export const POST: RequestHandler = async ({ locals, url }) => {
|
|||
SELECT
|
||||
a.alias AS model_name,
|
||||
p.litellm_model,
|
||||
p.api_key_env
|
||||
p.api_key_env,
|
||||
p.extra_params
|
||||
FROM ai_model_aliases a
|
||||
JOIN ai_model_providers p ON p.alias_id = a.id
|
||||
WHERE a.is_active = true AND p.is_active = true
|
||||
|
|
@ -35,6 +36,12 @@ export const POST: RequestHandler = async ({ locals, url }) => {
|
|||
yaml += ` litellm_params:\n`;
|
||||
yaml += ` model: "${row.litellm_model}"\n`;
|
||||
yaml += ` api_key: "os.environ/${row.api_key_env}"\n`;
|
||||
// Flett inn extra_params som ekstra nøkler under litellm_params
|
||||
if (row.extra_params && typeof row.extra_params === 'object') {
|
||||
for (const [key, value] of Object.entries(row.extra_params)) {
|
||||
yaml += ` ${key}: ${JSON.stringify(value)}\n`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
yaml += '\nrouter_settings:\n';
|
||||
|
|
|
|||
|
|
@ -6,15 +6,15 @@ import { sql } from '$lib/server/db';
|
|||
export const POST: RequestHandler = async ({ request, locals }) => {
|
||||
if (!locals.workspace || !locals.user) error(401);
|
||||
|
||||
const { alias_id, priority, litellm_model, api_key_env } = await request.json();
|
||||
const { alias_id, priority, litellm_model, api_key_env, extra_params } = await request.json();
|
||||
if (!alias_id || !litellm_model || !api_key_env) {
|
||||
error(400, 'alias_id, litellm_model og api_key_env er påkrevd');
|
||||
}
|
||||
|
||||
const [row] = await sql`
|
||||
INSERT INTO ai_model_providers (alias_id, priority, litellm_model, api_key_env)
|
||||
VALUES (${alias_id}::uuid, ${priority ?? 99}, ${litellm_model}, ${api_key_env})
|
||||
RETURNING id, alias_id, priority, litellm_model, api_key_env, is_active
|
||||
INSERT INTO ai_model_providers (alias_id, priority, litellm_model, api_key_env, extra_params)
|
||||
VALUES (${alias_id}::uuid, ${priority ?? 99}, ${litellm_model}, ${api_key_env}, ${extra_params ? sql.json(extra_params) : null})
|
||||
RETURNING id, alias_id, priority, litellm_model, api_key_env, is_active, extra_params
|
||||
`;
|
||||
|
||||
return json(row, { status: 201 });
|
||||
|
|
|
|||
|
|
@ -8,15 +8,20 @@ export const PATCH: RequestHandler = async ({ params, request, locals }) => {
|
|||
|
||||
const body = await request.json();
|
||||
|
||||
const extraParams = 'extra_params' in body
|
||||
? (body.extra_params ? sql.json(body.extra_params) : null)
|
||||
: undefined;
|
||||
|
||||
const [row] = await sql`
|
||||
UPDATE ai_model_providers SET
|
||||
priority = COALESCE(${body.priority ?? null}, priority),
|
||||
litellm_model = COALESCE(${body.litellm_model ?? null}, litellm_model),
|
||||
api_key_env = COALESCE(${body.api_key_env ?? null}, api_key_env),
|
||||
is_active = COALESCE(${body.is_active ?? null}, is_active),
|
||||
extra_params = ${extraParams !== undefined ? extraParams : sql`extra_params`},
|
||||
updated_at = now()
|
||||
WHERE id = ${params.id}::uuid
|
||||
RETURNING id, alias_id, priority, litellm_model, api_key_env, is_active
|
||||
RETURNING id, alias_id, priority, litellm_model, api_key_env, is_active, extra_params
|
||||
`;
|
||||
|
||||
if (!row) error(404, 'Provider ikke funnet');
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export const load: PageServerLoad = async () => {
|
|||
`;
|
||||
|
||||
const providers = await sql`
|
||||
SELECT id, alias_id, priority, litellm_model, api_key_env, is_active
|
||||
SELECT id, alias_id, priority, litellm_model, api_key_env, is_active, extra_params
|
||||
FROM ai_model_providers
|
||||
ORDER BY alias_id, priority
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
litellm_model: string;
|
||||
api_key_env: string;
|
||||
is_active: boolean;
|
||||
extra_params: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
interface Route {
|
||||
|
|
@ -94,6 +95,10 @@
|
|||
let showCatalogPicker = $state(false);
|
||||
let catalogPickerSearch = $state('');
|
||||
|
||||
// Extra params redigering
|
||||
let editingExtraParams = $state<string | null>(null);
|
||||
let editExtraParamsText = $state('');
|
||||
|
||||
// Ny provider-form
|
||||
let newProvider = $state<{ alias_id: string; litellm_model: string; api_key_env: string }>({
|
||||
alias_id: '',
|
||||
|
|
@ -314,6 +319,66 @@
|
|||
return hasAny ? total : null;
|
||||
});
|
||||
|
||||
function hasWebPlugin(provider: Provider): boolean {
|
||||
const plugins = provider.extra_params?.plugins;
|
||||
return Array.isArray(plugins) && plugins.some((p: any) => p.id === 'web');
|
||||
}
|
||||
|
||||
async function toggleWebPlugin(provider: Provider) {
|
||||
const has = hasWebPlugin(provider);
|
||||
let newParams: Record<string, unknown> | null;
|
||||
if (has) {
|
||||
const { plugins, ...rest } = provider.extra_params ?? {};
|
||||
newParams = Object.keys(rest).length > 0 ? rest : null;
|
||||
} else {
|
||||
newParams = { ...(provider.extra_params ?? {}), plugins: [{ id: 'web' }] };
|
||||
}
|
||||
saving = provider.id;
|
||||
errorMsg = '';
|
||||
try {
|
||||
const res = await fetch(`/api/admin/ai/providers/${provider.id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ extra_params: newParams })
|
||||
});
|
||||
if (!res.ok) throw new Error('Feil');
|
||||
const updated = await res.json();
|
||||
provider.extra_params = updated.extra_params;
|
||||
markSaved(provider.id);
|
||||
} catch {
|
||||
errorMsg = 'Kunne ikke oppdatere web-plugin';
|
||||
} finally {
|
||||
saving = null;
|
||||
}
|
||||
}
|
||||
|
||||
function startEditExtraParams(provider: Provider) {
|
||||
editingExtraParams = provider.id;
|
||||
editExtraParamsText = provider.extra_params ? JSON.stringify(provider.extra_params, null, 2) : '';
|
||||
}
|
||||
|
||||
async function saveExtraParams(provider: Provider) {
|
||||
saving = provider.id;
|
||||
errorMsg = '';
|
||||
try {
|
||||
const parsed = editExtraParamsText.trim() ? JSON.parse(editExtraParamsText) : null;
|
||||
const res = await fetch(`/api/admin/ai/providers/${provider.id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ extra_params: parsed })
|
||||
});
|
||||
if (!res.ok) throw new Error('Feil');
|
||||
const updated = await res.json();
|
||||
provider.extra_params = updated.extra_params;
|
||||
editingExtraParams = null;
|
||||
markSaved(provider.id);
|
||||
} catch (e: any) {
|
||||
errorMsg = e.message === 'Feil' ? 'Kunne ikke lagre params' : `Ugyldig JSON: ${e.message}`;
|
||||
} finally {
|
||||
saving = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleProvider(provider: Provider) {
|
||||
saving = provider.id;
|
||||
errorMsg = '';
|
||||
|
|
@ -650,6 +715,18 @@
|
|||
<span class="col-pri">#{provider.priority}</span>
|
||||
<span class="col-model">{provider.litellm_model}</span>
|
||||
<span class="col-key">{provider.api_key_env}</span>
|
||||
<span class="col-extra-pills">
|
||||
<button
|
||||
class="extra-pill"
|
||||
class:extra-pill--on={hasWebPlugin(provider)}
|
||||
onclick={() => toggleWebPlugin(provider)}
|
||||
title="Web-søk (OpenRouter plugin)"
|
||||
>web {hasWebPlugin(provider) ? '\u2713' : '\u2717'}</button>
|
||||
{#if provider.extra_params && Object.keys(provider.extra_params).length > (hasWebPlugin(provider) ? 1 : 0)}
|
||||
<span class="extra-pill extra-pill--custom" title={JSON.stringify(provider.extra_params)}>+params</span>
|
||||
{/if}
|
||||
<button class="extra-edit-btn" onclick={() => startEditExtraParams(provider)} title="Rediger extra_params JSON">…</button>
|
||||
</span>
|
||||
<span class="col-active">
|
||||
<button class="toggle-btn" onclick={() => toggleProvider(provider)}>
|
||||
{provider.is_active ? 'På' : 'Av'}
|
||||
|
|
@ -664,6 +741,19 @@
|
|||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
{#if editingExtraParams === provider.id}
|
||||
<div class="extra-params-editor">
|
||||
<textarea
|
||||
bind:value={editExtraParamsText}
|
||||
rows="4"
|
||||
placeholder={'{"plugins": [{"id": "web"}]}'}
|
||||
></textarea>
|
||||
<div class="extra-params-actions">
|
||||
<button class="add-btn" onclick={() => saveExtraParams(provider)}>Lagre</button>
|
||||
<button class="toggle-btn" onclick={() => { editingExtraParams = null; }}>Avbryt</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
<div class="provider-row-alias provider-row--add">
|
||||
|
|
@ -1192,7 +1282,7 @@
|
|||
|
||||
.provider-row-alias {
|
||||
display: grid;
|
||||
grid-template-columns: 30px 2fr 1.5fr 60px 60px 40px;
|
||||
grid-template-columns: 30px 2fr 1fr auto 50px 50px 40px;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.35rem 0;
|
||||
|
|
@ -1290,6 +1380,85 @@
|
|||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
/* Extra params */
|
||||
.col-extra-pills {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.extra-pill {
|
||||
font-size: 0.6rem;
|
||||
padding: 0.1rem 0.4rem;
|
||||
border-radius: 9999px;
|
||||
border: 1px solid #2d3148;
|
||||
background: #1a1d2e;
|
||||
color: #8b92a5;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.extra-pill:hover {
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
.extra-pill--on {
|
||||
background: #0d3320;
|
||||
border-color: #166534;
|
||||
color: #4ade80;
|
||||
}
|
||||
|
||||
.extra-pill--custom {
|
||||
background: #2d1f4e;
|
||||
border-color: #6d4aaa;
|
||||
color: #c4b5fd;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.extra-edit-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #8b92a5;
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
padding: 0 0.2rem;
|
||||
}
|
||||
|
||||
.extra-edit-btn:hover {
|
||||
color: #e1e4e8;
|
||||
}
|
||||
|
||||
.extra-params-editor {
|
||||
background: #161822;
|
||||
border: 1px solid #3b82f6;
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem;
|
||||
margin: 0.35rem 0;
|
||||
}
|
||||
|
||||
.extra-params-editor textarea {
|
||||
width: 100%;
|
||||
background: #0f1117;
|
||||
border: 1px solid #2d3148;
|
||||
border-radius: 4px;
|
||||
color: #e1e4e8;
|
||||
padding: 0.4rem;
|
||||
font-size: 0.75rem;
|
||||
font-family: monospace;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.extra-params-editor textarea:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
.extra-params-actions {
|
||||
display: flex;
|
||||
gap: 0.35rem;
|
||||
margin-top: 0.35rem;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.toggle-btn {
|
||||
background: #1a1d2e;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue