diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml
index dc90a24..6db6e73 100644
--- a/docker-compose.dev.yml
+++ b/docker-compose.dev.yml
@@ -54,7 +54,7 @@ services:
networks:
- sidelinja-dev
healthcheck:
- test: ["CMD-SHELL", "curl -f http://localhost:3000/database/ping || exit 1"]
+ test: ["CMD-SHELL", "curl -f http://localhost:3000/v1/ping || exit 1"]
interval: 10s
timeout: 5s
retries: 5
@@ -68,8 +68,9 @@ services:
environment:
LITELLM_MASTER_KEY: ${LITELLM_MASTER_KEY}
GEMINI_API_KEY: ${GEMINI_API_KEY}
+ OPENROUTER_API_KEY: ${OPENROUTER_API_KEY}
volumes:
- - ./config/litellm/config.yaml:/etc/litellm/config.yaml:ro
+ - ./config/litellm:/etc/litellm
ports:
- "127.0.0.1:4000:4000"
networks:
diff --git a/migrations/0007_ai_config.sql b/migrations/0007_ai_config.sql
new file mode 100644
index 0000000..62ebd95
--- /dev/null
+++ b/migrations/0007_ai_config.sql
@@ -0,0 +1,78 @@
+-- 0007_ai_config.sql
+-- AI-administrasjon: modellaliaser, leverandører, jobbruting og tokenlogging.
+-- PG som source of truth for LiteLLM config-generering.
+
+BEGIN;
+
+-- === Modellaliaser (globale, ikke per workspace) ===
+CREATE TABLE ai_model_aliases (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ alias TEXT NOT NULL UNIQUE, -- f.eks. "sidelinja/rutine"
+ description TEXT, -- kort beskrivelse av aliaset
+ is_active BOOLEAN NOT NULL DEFAULT true,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+
+-- === Leverandør-modeller per alias med prioritet ===
+CREATE TABLE ai_model_providers (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ alias_id UUID NOT NULL REFERENCES ai_model_aliases(id) ON DELETE CASCADE,
+ priority INT NOT NULL DEFAULT 1, -- lavere = høyere prioritet i LiteLLM
+ litellm_model TEXT NOT NULL, -- f.eks. "gemini/gemini-2.5-flash-lite"
+ api_key_env TEXT NOT NULL, -- f.eks. "GEMINI_API_KEY"
+ is_active BOOLEAN NOT NULL DEFAULT true,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ UNIQUE (alias_id, priority)
+);
+
+-- === Jobbtype → alias mapping ===
+CREATE TABLE ai_job_routing (
+ job_type TEXT PRIMARY KEY, -- f.eks. "ai_text_process"
+ alias_id UUID NOT NULL REFERENCES ai_model_aliases(id) ON DELETE RESTRICT,
+ description TEXT,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+
+-- === Per-kall token-logging (workspace-scopet) ===
+CREATE TABLE ai_usage_log (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
+ job_id UUID REFERENCES job_queue(id) ON DELETE SET NULL,
+ job_type TEXT NOT NULL,
+ model_alias TEXT NOT NULL, -- alias brukt (snapshot)
+ model_actual TEXT, -- faktisk modell fra respons
+ prompt_tokens INT NOT NULL DEFAULT 0,
+ completion_tokens INT NOT NULL DEFAULT 0,
+ total_tokens INT NOT NULL DEFAULT 0,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+
+CREATE INDEX idx_ai_usage_log_workspace ON ai_usage_log (workspace_id, created_at DESC);
+CREATE INDEX idx_ai_usage_log_alias ON ai_usage_log (model_alias, created_at DESC);
+
+-- === Seed-data: matcher nåværende config.yaml ===
+
+-- Aliaser
+INSERT INTO ai_model_aliases (alias, description) VALUES
+ ('sidelinja/rutine', 'Billig, høyt volum — tekstrensing, faktauthenting, oversettelse'),
+ ('sidelinja/resonering', 'Presis, lav volum — kompleks analyse, research');
+
+-- Leverandører for sidelinja/rutine
+INSERT INTO ai_model_providers (alias_id, priority, litellm_model, api_key_env) VALUES
+ ((SELECT id FROM ai_model_aliases WHERE alias = 'sidelinja/rutine'), 1, 'gemini/gemini-2.5-flash-lite', 'GEMINI_API_KEY'),
+ ((SELECT id FROM ai_model_aliases WHERE alias = 'sidelinja/rutine'), 2, 'gemini/gemini-2.5-flash', 'GEMINI_API_KEY'),
+ ((SELECT id FROM ai_model_aliases WHERE alias = 'sidelinja/rutine'), 3, 'openrouter/google/gemini-2.5-flash-preview', 'OPENROUTER_API_KEY');
+
+-- Leverandører for sidelinja/resonering
+INSERT INTO ai_model_providers (alias_id, priority, litellm_model, api_key_env) VALUES
+ ((SELECT id FROM ai_model_aliases WHERE alias = 'sidelinja/resonering'), 1, 'openrouter/anthropic/claude-sonnet-4', 'OPENROUTER_API_KEY'),
+ ((SELECT id FROM ai_model_aliases WHERE alias = 'sidelinja/resonering'), 2, 'gemini/gemini-2.5-flash', 'GEMINI_API_KEY');
+
+-- Jobbruting
+INSERT INTO ai_job_routing (job_type, alias_id, description) VALUES
+ ('ai_text_process', (SELECT id FROM ai_model_aliases WHERE alias = 'sidelinja/rutine'), 'Tekstrensing og AI-behandling via ✨-knappen');
+
+COMMIT;
diff --git a/web/package-lock.json b/web/package-lock.json
index 9f04f89..2e708b0 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -25,6 +25,7 @@
"vite": "^8.0.0"
},
"devDependencies": {
+ "@types/node": "^25.5.0",
"svelte-check": "^4.4.5"
}
},
@@ -1435,6 +1436,16 @@
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
"license": "MIT"
},
+ "node_modules/@types/node": {
+ "version": "25.5.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz",
+ "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.18.0"
+ }
+ },
"node_modules/@types/resolve": {
"version": "1.20.2",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
@@ -2777,6 +2788,13 @@
"integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
"license": "MIT"
},
+ "node_modules/undici-types": {
+ "version": "7.18.2",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
+ "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
+ "devOptional": true,
+ "license": "MIT"
+ },
"node_modules/url-polyfill": {
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/url-polyfill/-/url-polyfill-1.1.14.tgz",
diff --git a/web/package.json b/web/package.json
index 5c6dff5..63b4f90 100644
--- a/web/package.json
+++ b/web/package.json
@@ -27,6 +27,7 @@
"vite": "^8.0.0"
},
"devDependencies": {
+ "@types/node": "^25.5.0",
"svelte-check": "^4.4.5"
}
}
diff --git a/web/src/routes/admin/ai/+page.server.ts b/web/src/routes/admin/ai/+page.server.ts
new file mode 100644
index 0000000..61027e1
--- /dev/null
+++ b/web/src/routes/admin/ai/+page.server.ts
@@ -0,0 +1,41 @@
+import { error } from '@sveltejs/kit';
+import type { PageServerLoad } from './$types';
+import { sql } from '$lib/server/db';
+
+export const load: PageServerLoad = async ({ locals }) => {
+ if (!locals.workspace) error(404);
+
+ const aliases = await sql`
+ SELECT id, alias, description, is_active, created_at
+ FROM ai_model_aliases
+ ORDER BY alias
+ `;
+
+ const providers = await sql`
+ SELECT id, alias_id, priority, litellm_model, api_key_env, is_active
+ FROM ai_model_providers
+ ORDER BY alias_id, priority
+ `;
+
+ const routing = await sql`
+ SELECT r.job_type, r.alias_id, r.description, a.alias
+ FROM ai_job_routing r
+ JOIN ai_model_aliases a ON a.id = r.alias_id
+ ORDER BY r.job_type
+ `;
+
+ const usage = await sql`
+ SELECT
+ model_alias,
+ count(*)::int AS call_count,
+ sum(prompt_tokens)::int AS prompt_tokens,
+ sum(completion_tokens)::int AS completion_tokens,
+ sum(total_tokens)::int AS total_tokens
+ FROM ai_usage_log
+ WHERE created_at > now() - interval '30 days'
+ GROUP BY model_alias
+ ORDER BY total_tokens DESC
+ `;
+
+ return { aliases, providers, routing, usage };
+};
diff --git a/web/src/routes/admin/ai/+page.svelte b/web/src/routes/admin/ai/+page.svelte
new file mode 100644
index 0000000..98fa617
--- /dev/null
+++ b/web/src/routes/admin/ai/+page.svelte
@@ -0,0 +1,668 @@
+
+
+
+
+
+ {#if errorMsg}
+
{errorMsg}
+ {/if}
+
+
+
+ Modellaliaser
+
+
+
+ {#each aliases as alias (alias.id)}
+ {@const ap = providersForAlias(alias.id)}
+
+ (expandedAlias = expandedAlias === alias.id ? null : alias.id)}
+ >
+ {alias.alias}
+
+ {alias.description ?? '—'}
+ {ap.length}
+
+
+
+
+ {#if saving === alias.id}
+ ...
+ {:else if saved === alias.id}
+ OK
+ {/if}
+
+
+
+ {#if expandedAlias === alias.id}
+
+ {#each ap as provider (provider.id)}
+
+ #{provider.priority}
+ {provider.litellm_model}
+ {provider.api_key_env}
+
+
+
+
+
+ {#if saving === provider.id}
+ ...
+ {:else if saved === provider.id}
+ OK
+ {/if}
+
+
+ {/each}
+
+
+
+
+
+
+
+
+ {/if}
+ {/each}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Tokenforbruk (siste 30 dager)
+ {#if usage.length === 0}
+ Ingen AI-kall registrert ennå.
+ {:else}
+
+
+
+ {#each usage as row}
+
+ {row.model_alias}
+ {row.call_count}
+ {row.prompt_tokens.toLocaleString('nb-NO')}
+ {row.completion_tokens.toLocaleString('nb-NO')}
+ {row.total_tokens.toLocaleString('nb-NO')}
+
+ {/each}
+
+ {/if}
+
+
+
+
+ Konfigurasjon
+
+
+ {#if configMsg}
+
{configMsg}
+ {/if}
+
Genererer LiteLLM config.yaml fra databasen. AI Gateway (LiteLLM) må restartes for å lese ny config.
+
+
+
+
+
diff --git a/web/src/routes/api/admin/ai/aliases/+server.ts b/web/src/routes/api/admin/ai/aliases/+server.ts
new file mode 100644
index 0000000..3f35544
--- /dev/null
+++ b/web/src/routes/api/admin/ai/aliases/+server.ts
@@ -0,0 +1,19 @@
+import { json, error } from '@sveltejs/kit';
+import type { RequestHandler } from './$types';
+import { sql } from '$lib/server/db';
+
+/** POST — opprett nytt alias */
+export const POST: RequestHandler = async ({ request, locals }) => {
+ if (!locals.workspace || !locals.user) error(401);
+
+ const { alias, description } = await request.json();
+ if (!alias || typeof alias !== 'string') error(400, 'alias er påkrevd');
+
+ const [row] = await sql`
+ INSERT INTO ai_model_aliases (alias, description)
+ VALUES (${alias}, ${description ?? null})
+ RETURNING id, alias, description, is_active, created_at
+ `;
+
+ return json(row, { status: 201 });
+};
diff --git a/web/src/routes/api/admin/ai/aliases/[id]/+server.ts b/web/src/routes/api/admin/ai/aliases/[id]/+server.ts
new file mode 100644
index 0000000..5ee83a1
--- /dev/null
+++ b/web/src/routes/api/admin/ai/aliases/[id]/+server.ts
@@ -0,0 +1,39 @@
+import { json, error } from '@sveltejs/kit';
+import type { RequestHandler } from './$types';
+import { sql } from '$lib/server/db';
+
+/** PATCH — oppdater alias */
+export const PATCH: RequestHandler = async ({ params, request, locals }) => {
+ if (!locals.workspace || !locals.user) error(401);
+
+ const body = await request.json();
+ const updates: Record = {};
+ if ('description' in body) updates.description = body.description;
+ if ('is_active' in body) updates.is_active = body.is_active;
+
+ const [row] = await sql`
+ UPDATE ai_model_aliases SET
+ description = COALESCE(${body.description ?? null}, description),
+ is_active = COALESCE(${body.is_active ?? null}, is_active),
+ updated_at = now()
+ WHERE id = ${params.id}::uuid
+ RETURNING id, alias, description, is_active
+ `;
+
+ if (!row) error(404, 'Alias ikke funnet');
+ return json(row);
+};
+
+/** DELETE — slett alias (feiler hvis brukt i routing) */
+export const DELETE: RequestHandler = async ({ params, locals }) => {
+ if (!locals.workspace || !locals.user) error(401);
+
+ try {
+ await sql`DELETE FROM ai_model_aliases WHERE id = ${params.id}::uuid`;
+ } catch (e: any) {
+ if (e.code === '23503') error(409, 'Aliaset er i bruk av jobbruting og kan ikke slettes');
+ throw e;
+ }
+
+ return json({ ok: true });
+};
diff --git a/web/src/routes/api/admin/ai/generate-config/+server.ts b/web/src/routes/api/admin/ai/generate-config/+server.ts
new file mode 100644
index 0000000..6625d57
--- /dev/null
+++ b/web/src/routes/api/admin/ai/generate-config/+server.ts
@@ -0,0 +1,54 @@
+import { json, error } from '@sveltejs/kit';
+import type { RequestHandler } from './$types';
+import { sql } from '$lib/server/db';
+import { writeFileSync } from 'node:fs';
+import { join } from 'node:path';
+
+/**
+ * POST /api/admin/ai/generate-config — Generer LiteLLM config.yaml fra PG.
+ */
+export const POST: RequestHandler = async ({ locals }) => {
+ if (!locals.workspace || !locals.user) error(401);
+
+ // Hent aktive aliaser med aktive providers
+ const rows = await sql`
+ SELECT
+ a.alias AS model_name,
+ p.litellm_model,
+ p.api_key_env
+ 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
+ ORDER BY a.alias, p.priority ASC
+ `;
+
+ if (rows.length === 0) {
+ error(400, 'Ingen aktive modellkonfigurasjoner funnet');
+ }
+
+ // Bygg YAML manuelt (unngå avhengighet)
+ let yaml = 'model_list:\n';
+ for (const row of rows) {
+ yaml += ` - model_name: "${row.model_name}"\n`;
+ yaml += ` litellm_params:\n`;
+ yaml += ` model: "${row.litellm_model}"\n`;
+ yaml += ` api_key: "os.environ/${row.api_key_env}"\n`;
+ }
+
+ yaml += '\nrouter_settings:\n';
+ yaml += ' routing_strategy: "simple-shuffle"\n';
+ yaml += ' num_retries: 2\n';
+ yaml += ' timeout: 60\n';
+ yaml += '\ngeneral_settings:\n';
+ yaml += ' master_key: "os.environ/LITELLM_MASTER_KEY"\n';
+
+ // Skriv til config-fil
+ const configPath = join(process.cwd(), 'config', 'litellm', 'config.yaml');
+ writeFileSync(configPath, yaml, 'utf-8');
+
+ return json({
+ ok: true,
+ message: 'Config generert. Restart ai-gateway for å aktivere.',
+ model_count: rows.length
+ });
+};
diff --git a/web/src/routes/api/admin/ai/providers/+server.ts b/web/src/routes/api/admin/ai/providers/+server.ts
new file mode 100644
index 0000000..9da97bb
--- /dev/null
+++ b/web/src/routes/api/admin/ai/providers/+server.ts
@@ -0,0 +1,21 @@
+import { json, error } from '@sveltejs/kit';
+import type { RequestHandler } from './$types';
+import { sql } from '$lib/server/db';
+
+/** POST — opprett ny provider for et alias */
+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();
+ 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
+ `;
+
+ 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
new file mode 100644
index 0000000..5611a72
--- /dev/null
+++ b/web/src/routes/api/admin/ai/providers/[id]/+server.ts
@@ -0,0 +1,32 @@
+import { json, error } from '@sveltejs/kit';
+import type { RequestHandler } from './$types';
+import { sql } from '$lib/server/db';
+
+/** PATCH — oppdater provider */
+export const PATCH: RequestHandler = async ({ params, request, locals }) => {
+ if (!locals.workspace || !locals.user) error(401);
+
+ const body = await request.json();
+
+ 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),
+ updated_at = now()
+ WHERE id = ${params.id}::uuid
+ RETURNING id, alias_id, priority, litellm_model, api_key_env, is_active
+ `;
+
+ if (!row) error(404, 'Provider ikke funnet');
+ return json(row);
+};
+
+/** DELETE — slett provider */
+export const DELETE: RequestHandler = async ({ params, locals }) => {
+ if (!locals.workspace || !locals.user) error(401);
+
+ await sql`DELETE FROM ai_model_providers WHERE id = ${params.id}::uuid`;
+ return json({ ok: true });
+};
diff --git a/web/src/routes/api/admin/ai/routing/+server.ts b/web/src/routes/api/admin/ai/routing/+server.ts
new file mode 100644
index 0000000..1392cd0
--- /dev/null
+++ b/web/src/routes/api/admin/ai/routing/+server.ts
@@ -0,0 +1,23 @@
+import { json, error } from '@sveltejs/kit';
+import type { RequestHandler } from './$types';
+import { sql } from '$lib/server/db';
+
+/** POST — opprett ny jobbruting */
+export const POST: RequestHandler = async ({ request, locals }) => {
+ if (!locals.workspace || !locals.user) error(401);
+
+ const { job_type, alias_id, description } = await request.json();
+ if (!job_type || !alias_id) error(400, 'job_type og alias_id er påkrevd');
+
+ const [row] = await sql`
+ INSERT INTO ai_job_routing (job_type, alias_id, description)
+ VALUES (${job_type}, ${alias_id}::uuid, ${description ?? null})
+ ON CONFLICT (job_type) DO UPDATE SET
+ alias_id = EXCLUDED.alias_id,
+ description = EXCLUDED.description,
+ updated_at = now()
+ RETURNING job_type, alias_id, description
+ `;
+
+ return json(row, { status: 201 });
+};
diff --git a/web/src/routes/api/admin/ai/routing/[jobType]/+server.ts b/web/src/routes/api/admin/ai/routing/[jobType]/+server.ts
new file mode 100644
index 0000000..6375bbe
--- /dev/null
+++ b/web/src/routes/api/admin/ai/routing/[jobType]/+server.ts
@@ -0,0 +1,30 @@
+import { json, error } from '@sveltejs/kit';
+import type { RequestHandler } from './$types';
+import { sql } from '$lib/server/db';
+
+/** PATCH — oppdater ruting for jobbtype */
+export const PATCH: RequestHandler = async ({ params, request, locals }) => {
+ if (!locals.workspace || !locals.user) error(401);
+
+ const body = await request.json();
+
+ const [row] = await sql`
+ UPDATE ai_job_routing SET
+ alias_id = COALESCE(${body.alias_id ?? null}::uuid, alias_id),
+ description = COALESCE(${body.description ?? null}, description),
+ updated_at = now()
+ WHERE job_type = ${params.jobType}
+ RETURNING job_type, alias_id, description
+ `;
+
+ if (!row) error(404, 'Jobbtype ikke funnet');
+ return json(row);
+};
+
+/** DELETE — slett ruting */
+export const DELETE: RequestHandler = async ({ params, locals }) => {
+ if (!locals.workspace || !locals.user) error(401);
+
+ await sql`DELETE FROM ai_job_routing WHERE job_type = ${params.jobType}`;
+ return json({ ok: true });
+};
diff --git a/web/src/routes/api/ai/usage/+server.ts b/web/src/routes/api/ai/usage/+server.ts
new file mode 100644
index 0000000..adbb0ba
--- /dev/null
+++ b/web/src/routes/api/ai/usage/+server.ts
@@ -0,0 +1,31 @@
+import { json, error } from '@sveltejs/kit';
+import type { RequestHandler } from './$types';
+import { sql } from '$lib/server/db';
+
+/**
+ * GET /api/ai/usage?days=30 — Aggregert tokenforbruk for workspace.
+ */
+export const GET: RequestHandler = async ({ url, locals }) => {
+ if (!locals.workspace || !locals.user) error(401);
+
+ const days = Math.min(Math.max(parseInt(url.searchParams.get('days') ?? '7'), 1), 365);
+
+ const breakdown = await sql`
+ SELECT
+ model_alias,
+ job_type,
+ count(*)::int AS call_count,
+ sum(prompt_tokens)::int AS prompt_tokens,
+ sum(completion_tokens)::int AS completion_tokens,
+ sum(total_tokens)::int AS total_tokens
+ FROM ai_usage_log
+ WHERE workspace_id = ${locals.workspace.id}
+ AND created_at > now() - make_interval(days => ${days})
+ GROUP BY model_alias, job_type
+ ORDER BY total_tokens DESC
+ `;
+
+ const total_tokens = breakdown.reduce((s: number, r: any) => s + r.total_tokens, 0);
+
+ return json({ total_tokens, breakdown });
+};
diff --git a/worker/src/handlers/ai_text_process.rs b/worker/src/handlers/ai_text_process.rs
index cef6281..ccc25ab 100644
--- a/worker/src/handlers/ai_text_process.rs
+++ b/worker/src/handlers/ai_text_process.rs
@@ -5,6 +5,15 @@ use sqlx::{PgPool, Row};
use tracing::{info, warn};
use uuid::Uuid;
+/// Respons fra AI Gateway med innhold og tokenforbruk.
+struct AiResponse {
+ content: String,
+ prompt_tokens: i32,
+ completion_tokens: i32,
+ total_tokens: i32,
+ model_actual: Option,
+}
+
/// Handler for AI-behandling av tekst i editoren.
///
/// Payload:
@@ -21,6 +30,7 @@ use uuid::Uuid;
/// 3. Send til AI Gateway med riktig prompt
/// 4. Oppdater messages.body med AI-resultatet
/// 5. Sett metadata.ai_processed = true
+/// 6. Logg tokenforbruk til ai_usage_log
pub struct AiTextProcessHandler {
http: reqwest::Client,
ai_gateway_url: String,
@@ -41,6 +51,7 @@ impl JobHandler for AiTextProcessHandler {
&self,
pool: &PgPool,
workspace_id: &Uuid,
+ job_id: &Uuid,
payload: &Value,
) -> anyhow::Result