diff --git a/migrations/0009_provider_extra_params.sql b/migrations/0009_provider_extra_params.sql new file mode 100644 index 0000000..7593b43 --- /dev/null +++ b/migrations/0009_provider_extra_params.sql @@ -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; diff --git a/web/src/routes/api/admin/ai/generate-config/+server.ts b/web/src/routes/api/admin/ai/generate-config/+server.ts index f64644a..bc7dd3c 100644 --- a/web/src/routes/api/admin/ai/generate-config/+server.ts +++ b/web/src/routes/api/admin/ai/generate-config/+server.ts @@ -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'; diff --git a/web/src/routes/api/admin/ai/providers/+server.ts b/web/src/routes/api/admin/ai/providers/+server.ts index 9da97bb..fd99e2b 100644 --- a/web/src/routes/api/admin/ai/providers/+server.ts +++ b/web/src/routes/api/admin/ai/providers/+server.ts @@ -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 }); diff --git a/web/src/routes/api/admin/ai/providers/[id]/+server.ts b/web/src/routes/api/admin/ai/providers/[id]/+server.ts index 5611a72..7ce32c2 100644 --- a/web/src/routes/api/admin/ai/providers/[id]/+server.ts +++ b/web/src/routes/api/admin/ai/providers/[id]/+server.ts @@ -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'); diff --git a/web/src/routes/server-admin/ai/+page.server.ts b/web/src/routes/server-admin/ai/+page.server.ts index 00e04f7..1f564d9 100644 --- a/web/src/routes/server-admin/ai/+page.server.ts +++ b/web/src/routes/server-admin/ai/+page.server.ts @@ -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 `; diff --git a/web/src/routes/server-admin/ai/+page.svelte b/web/src/routes/server-admin/ai/+page.svelte index 84fc70e..077b9fe 100644 --- a/web/src/routes/server-admin/ai/+page.svelte +++ b/web/src/routes/server-admin/ai/+page.svelte @@ -17,6 +17,7 @@ litellm_model: string; api_key_env: string; is_active: boolean; + extra_params: Record | null; } interface Route { @@ -94,6 +95,10 @@ let showCatalogPicker = $state(false); let catalogPickerSearch = $state(''); + // Extra params redigering + let editingExtraParams = $state(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 | 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 @@ #{provider.priority} {provider.litellm_model} {provider.api_key_env} + + + {#if provider.extra_params && Object.keys(provider.extra_params).length > (hasWebPlugin(provider) ? 1 : 0)} + +params + {/if} + + + + + + {/if} {/each}
@@ -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;