487 lines
22 KiB
Plaintext
487 lines
22 KiB
Plaintext
|
|
/**
|
||
|
|
* Wavult AI Model Router
|
||
|
|
* ─────────────────────────────────────────────────────────────────────
|
||
|
|
* Prioritet:
|
||
|
|
* 1. Qwen3 via AWS Bedrock (selfhostad, eu-north-1) — PRIMÄR
|
||
|
|
* 2. Gemini 2.5 Pro via Google API — BACKUP / MULTIMODAL
|
||
|
|
* 3. Claude Sonnet via Anthropic — LEGACY FALLBACK (fasas ut)
|
||
|
|
*
|
||
|
|
* Val per uppgift:
|
||
|
|
* AUDIT → Qwen3 235B (djup analys, hög precision)
|
||
|
|
* OPERATOR → Qwen3 32B (snabb, generell)
|
||
|
|
* CODE → Qwen3 Coder 480B (världsklass på kod)
|
||
|
|
* GEMINI → Gemini 2.5 Pro (explicit val eller multimodal)
|
||
|
|
* CLAUDE → Claude Sonnet (legacy fallback)
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { BedrockRuntimeClient, InvokeModelCommand, InvokeModelWithResponseStreamCommand } from '@aws-sdk/client-bedrock-runtime';
|
||
|
|
// ── GECL Universal Enforcement (G-002 2026-06-05) ───────────────────────
|
||
|
|
let _geclMW = null;
|
||
|
|
async function _getGeclMW() {
|
||
|
|
if (_geclMW) return _geclMW;
|
||
|
|
try { _geclMW = await import('./core/gecl-middleware.mjs'); } catch { _geclMW = {}; }
|
||
|
|
return _geclMW;
|
||
|
|
}
|
||
|
|
async function _wrapLLM(fn, model, messages) {
|
||
|
|
const mw = await _getGeclMW();
|
||
|
|
if (!mw?.wrapLLM) return fn();
|
||
|
|
try { return await mw.wrapLLM(fn, { model: model || 'unknown', messages: messages || [] }); }
|
||
|
|
catch(e) { if (e?.code === 'ENFORCEMENT_DENY') throw e; return fn(); }
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
const bedrock = new BedrockRuntimeClient({ region: process.env.AWS_REGION || 'eu-north-1' });
|
||
|
|
|
||
|
|
// ── Modell-IDs ────────────────────────────────────────────────────────────
|
||
|
|
export const MODELS = {
|
||
|
|
QWEN_235B: 'qwen.qwen3-235b-a22b-2507-v1:0',
|
||
|
|
QWEN_32B: 'qwen.qwen3-32b-v1:0',
|
||
|
|
QWEN_CODER: 'qwen.qwen3-coder-480b-a35b-v1:0',
|
||
|
|
QWEN_CODER_S: 'qwen.qwen3-coder-30b-a3b-v1:0',
|
||
|
|
|
||
|
|
// ── Ollama — local inference (qwen2.5:7b, CPU) ─────────────────────────────
|
||
|
|
export async function ollamaChat(model = 'qwen2.5:7b', system, messages, maxTokens = 512) {
|
||
|
|
const msgs = [...(system ? [{ role: 'system', content: system }] : []), ...messages];
|
||
|
|
const r = await fetch('http://localhost:11434/api/chat', {
|
||
|
|
method: 'POST',
|
||
|
|
headers: { 'Content-Type': 'application/json' },
|
||
|
|
body: JSON.stringify({ model, messages: msgs, stream: false, options: { num_predict: maxTokens } }),
|
||
|
|
signal: AbortSignal.timeout(120000)
|
||
|
|
});
|
||
|
|
const d = await r.json();
|
||
|
|
if (!r.ok) throw new Error(`ollama ${r.status}: ${d.error || ''}`);
|
||
|
|
return { text: d.message?.content || '', model };
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Aletheia v6 — ESLM (Evidence Small Language Model) ──────────────────
|
||
|
|
// Importerad via Bedrock custom model import 2026-06-07
|
||
|
|
// Red-team: 100% (Opus 4.8 domare), 0 kritiska, gate PASS
|
||
|
|
ALETHEIA_V6: 'arn:aws:bedrock:us-east-1:155407238699:imported-model/29nyfw6n1prl',
|
||
|
|
};
|
||
|
|
|
||
|
|
// ── Välj modell baserat på läge och kontext ──────────────────────────────
|
||
|
|
export function selectModel(mode, message = '') {
|
||
|
|
const isCode = /```|function|class |def |import |SELECT|CREATE TABLE/i.test(message);
|
||
|
|
const isComplex = message.length > 500 || /analysera|granska|bedöm|jämför|rapport/i.test(message);
|
||
|
|
|
||
|
|
if (mode === 'AUDIT') return MODELS.QWEN_235B;
|
||
|
|
if (isCode) return MODELS.QWEN_CODER; // 30B snabbare för vanlig kod
|
||
|
|
if (isComplex) return MODELS.QWEN_32B;
|
||
|
|
return MODELS.QWEN_32B; // default
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── COMPLIANCE-AWARE MODEL SELECTION (sedan 2026-04-30) ─────────────────────
|
||
|
|
// Väljer modell men validerar mot tenant compliance-mode innan den returneras.
|
||
|
|
// Throws vid violation. Använd från endpoints där compliance är kritiskt.
|
||
|
|
export async function selectModelCompliant(complianceMode, mode, message = '', context = {}) {
|
||
|
|
const { enforceModel, checkModel } = await import('./compliance/engine.mjs');
|
||
|
|
let candidate = selectModel(mode, message);
|
||
|
|
const familyName = mapModelIdToFamily(candidate);
|
||
|
|
|
||
|
|
try {
|
||
|
|
enforceModel(complianceMode, familyName, { mode, source: 'model-router', ...context });
|
||
|
|
return candidate;
|
||
|
|
} catch (e) {
|
||
|
|
const allowed = await getCompliantFallback(complianceMode, mode);
|
||
|
|
if (allowed) {
|
||
|
|
console.warn(JSON.stringify({
|
||
|
|
ts: new Date().toISOString(), level: 'warn', component: 'model-router',
|
||
|
|
msg: 'compliance_fallback', from_model: candidate, to_model: allowed,
|
||
|
|
compliance_mode: complianceMode, reason: e.message,
|
||
|
|
}));
|
||
|
|
return allowed;
|
||
|
|
}
|
||
|
|
throw e;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function mapModelIdToFamily(modelId) {
|
||
|
|
const id = String(modelId).toLowerCase();
|
||
|
|
if (id.includes('qwen3-235b')) return 'qwen3-235b';
|
||
|
|
if (id.includes('qwen3-coder-480b')) return 'qwen3-coder-480b';
|
||
|
|
if (id.includes('qwen3-coder')) return 'qwen3-coder';
|
||
|
|
if (id.includes('qwen3-32b')) return 'qwen3-32b';
|
||
|
|
if (id.includes('qwen')) return 'qwen3';
|
||
|
|
if (id.includes('claude-opus')) return 'claude-opus';
|
||
|
|
if (id.includes('claude-sonnet')) return 'claude-sonnet';
|
||
|
|
if (id.includes('claude')) return 'claude';
|
||
|
|
if (id.includes('gpt-4')) return 'gpt-4';
|
||
|
|
if (id.includes('gpt')) return 'gpt';
|
||
|
|
if (id.includes('gemini')) return 'gemini';
|
||
|
|
if (id.includes('deepseek')) return 'deepseek';
|
||
|
|
if (id.includes('mistral')) return 'mistral';
|
||
|
|
if (id.includes('llama')) return 'llama';
|
||
|
|
return id;
|
||
|
|
}
|
||
|
|
|
||
|
|
async function getCompliantFallback(complianceMode, taskMode) {
|
||
|
|
const fallbackChain = {
|
||
|
|
'commercial-eu': [MODELS.QWEN_32B, 'claude-sonnet', 'gpt-4', 'mistral-large-2'],
|
||
|
|
'sovereign-cn': [MODELS.QWEN_32B, MODELS.QWEN_235B, 'deepseek-r2', 'mistral-large-2'],
|
||
|
|
'federal-us': ['claude-opus-4-7', 'claude-sonnet', 'gpt-4', 'mistral-large-2', 'llama-3.3-70b'],
|
||
|
|
'state-us': ['claude-opus-4-7', 'claude-sonnet', 'gpt-4', 'mistral-large-2', 'llama-3.3-70b'],
|
||
|
|
'defense-us': ['claude-gov', 'gpt-gov', 'llama-3.3-70b'],
|
||
|
|
};
|
||
|
|
const candidates = fallbackChain[complianceMode] || [];
|
||
|
|
const { checkModel } = await import('./compliance/engine.mjs');
|
||
|
|
for (const c of candidates) {
|
||
|
|
const r = checkModel(complianceMode, mapModelIdToFamily(c));
|
||
|
|
if (r.allowed) return c;
|
||
|
|
}
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Qwen streaming via Bedrock ────────────────────────────────────────────
|
||
|
|
export async function qwenStream(model, systemPrompt, messages, maxTokens, onChunk) {
|
||
|
|
const body = JSON.stringify({
|
||
|
|
messages: [
|
||
|
|
...(systemPrompt ? [{ role: 'system', content: systemPrompt }] : []),
|
||
|
|
...messages.map(m => ({ role: m.role, content: typeof m.content === 'string' ? m.content : extractText(m.content) }))
|
||
|
|
],
|
||
|
|
max_tokens: maxTokens || 4096,
|
||
|
|
temperature: 0.7,
|
||
|
|
stream: true
|
||
|
|
});
|
||
|
|
|
||
|
|
const cmd = new InvokeModelWithResponseStreamCommand({
|
||
|
|
modelId: model,
|
||
|
|
body: Buffer.from(body),
|
||
|
|
contentType: 'application/json',
|
||
|
|
accept: 'application/json'
|
||
|
|
});
|
||
|
|
|
||
|
|
const response = await bedrock.send(cmd);
|
||
|
|
let fullText = '';
|
||
|
|
|
||
|
|
for await (const event of response.body) {
|
||
|
|
if (event.chunk?.bytes) {
|
||
|
|
try {
|
||
|
|
const chunk = JSON.parse(Buffer.from(event.chunk.bytes).toString());
|
||
|
|
// OpenAI-kompatibelt format (Bedrock Qwen returnerar detta)
|
||
|
|
const delta = chunk?.choices?.[0]?.delta?.content || '';
|
||
|
|
if (delta) {
|
||
|
|
fullText += delta;
|
||
|
|
onChunk(delta);
|
||
|
|
}
|
||
|
|
} catch {}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return fullText;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Qwen icke-streaming (för tool-use loops) ─────────────────────────────
|
||
|
|
export async function qwenCreate(model, systemPrompt, messages, maxTokens) { return _wrapLLM(async () => {
|
||
|
|
const body = JSON.stringify({
|
||
|
|
messages: [
|
||
|
|
...(systemPrompt ? [{ role: 'system', content: systemPrompt }] : []),
|
||
|
|
...messages.map(m => ({ role: m.role, content: typeof m.content === 'string' ? m.content : extractText(m.content) }))
|
||
|
|
],
|
||
|
|
max_tokens: maxTokens || 4096,
|
||
|
|
temperature: 0.7
|
||
|
|
});
|
||
|
|
|
||
|
|
const cmd = new InvokeModelCommand({
|
||
|
|
modelId: model,
|
||
|
|
body: Buffer.from(body),
|
||
|
|
contentType: 'application/json',
|
||
|
|
accept: 'application/json'
|
||
|
|
});
|
||
|
|
|
||
|
|
const response = await bedrock.send(cmd);
|
||
|
|
const result = JSON.parse(Buffer.from(response.body).toString());
|
||
|
|
const text = result?.choices?.[0]?.message?.content || '';
|
||
|
|
return text;
|
||
|
|
}, model, messages); } // gecl wrap end
|
||
|
|
|
||
|
|
// ── Gemini via Google API ─────────────────────────────────────────────────
|
||
|
|
export async function geminiCreate(systemPrompt, messages, maxTokens) {
|
||
|
|
const GEMINI_KEY = process.env.GEMINI_API_KEY;
|
||
|
|
if (!GEMINI_KEY) throw new Error('GEMINI_API_KEY saknas');
|
||
|
|
|
||
|
|
const contents = messages.map(m => ({
|
||
|
|
role: m.role === 'assistant' ? 'model' : 'user',
|
||
|
|
parts: [{ text: typeof m.content === 'string' ? m.content : extractText(m.content) }]
|
||
|
|
}));
|
||
|
|
|
||
|
|
const body = {
|
||
|
|
systemInstruction: systemPrompt ? { parts: [{ text: systemPrompt }] } : undefined,
|
||
|
|
contents,
|
||
|
|
generationConfig: { maxOutputTokens: maxTokens || 4096, temperature: 0.7 }
|
||
|
|
};
|
||
|
|
|
||
|
|
const resp = await fetch(
|
||
|
|
`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro-preview-05-06:generateContent?key=${GEMINI_KEY}`,
|
||
|
|
{ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }
|
||
|
|
);
|
||
|
|
const data = await resp.json();
|
||
|
|
return data?.candidates?.[0]?.content?.parts?.[0]?.text || '';
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Hjälpfunktion: extrahera text från Anthropic content-blocks ──────────
|
||
|
|
function extractText(content) {
|
||
|
|
if (!content) return '';
|
||
|
|
if (typeof content === 'string') return content;
|
||
|
|
if (Array.isArray(content)) {
|
||
|
|
return content
|
||
|
|
.filter(b => b.type === 'text' || b.type === 'tool_result')
|
||
|
|
.map(b => b.text || (Array.isArray(b.content) ? b.content.map(c => c.text || '').join(' ') : '') )
|
||
|
|
.join('\n');
|
||
|
|
}
|
||
|
|
return String(content);
|
||
|
|
}
|
||
|
|
|
||
|
|
export default { MODELS, selectModel, qwenStream, qwenCreate, geminiCreate };
|
||
|
|
|
||
|
|
// ── Gemini Vision — bildanalys ────────────────────────────────────────────────
|
||
|
|
export async function geminiVision(prompt, base64, mimeType = 'image/png', systemInstruction = '') {
|
||
|
|
const GEMINI_KEY = process.env.GEMINI_API_KEY || '';
|
||
|
|
if (!GEMINI_KEY) throw new Error('GEMINI_API_KEY saknas');
|
||
|
|
|
||
|
|
const body = {
|
||
|
|
contents: [{
|
||
|
|
role: 'user',
|
||
|
|
parts: [
|
||
|
|
{ text: prompt },
|
||
|
|
{ inline_data: { mime_type: mimeType, data: base64 } }
|
||
|
|
]
|
||
|
|
}],
|
||
|
|
...(systemInstruction ? { system_instruction: { parts: [{ text: systemInstruction }] } } : {}),
|
||
|
|
generationConfig: { maxOutputTokens: 4096, temperature: 0.7 }
|
||
|
|
};
|
||
|
|
|
||
|
|
const r = await fetch(
|
||
|
|
`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro-preview-05-06:generateContent?key=${GEMINI_KEY}`,
|
||
|
|
{ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }
|
||
|
|
);
|
||
|
|
const d = await r.json();
|
||
|
|
if (!r.ok) throw new Error(d.error?.message || 'Gemini Vision fel');
|
||
|
|
return d.candidates?.[0]?.content?.parts?.[0]?.text || '';
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Gemini Design (alias för Vision med design-prompt) ────────────────────────
|
||
|
|
export async function geminiDesign(prompt, base64, mimeType = 'image/png') {
|
||
|
|
return geminiVision(prompt, base64, mimeType, 'Du är Wavults design-expert. Analysera och ge konkreta förbättringsförslag med kod.');
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Imagen (stub) ─────────────────────────────────────────────────────────────
|
||
|
|
export async function imagenGenerate(prompt) {
|
||
|
|
throw new Error('Imagen ej aktiverad i denna miljö');
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Perplexity — realtidsfakta ────────────────────────────────────────────────
|
||
|
|
export async function perplexitySearch(query, systemPrompt = '') {
|
||
|
|
const KEY = process.env.PERPLEXITY_API_KEY || '';
|
||
|
|
if (!KEY) throw new Error('PERPLEXITY_API_KEY saknas');
|
||
|
|
|
||
|
|
const body = {
|
||
|
|
model: 'sonar-pro',
|
||
|
|
messages: [
|
||
|
|
...(systemPrompt ? [{ role: 'system', content: systemPrompt }] : []),
|
||
|
|
{ role: 'user', content: query }
|
||
|
|
],
|
||
|
|
max_tokens: 2048,
|
||
|
|
return_citations: true,
|
||
|
|
return_related_questions: false,
|
||
|
|
search_recency_filter: 'week'
|
||
|
|
};
|
||
|
|
|
||
|
|
const r = await fetch('https://api.perplexity.ai/chat/completions', {
|
||
|
|
method: 'POST',
|
||
|
|
headers: { 'Authorization': `Bearer ${KEY}`, 'Content-Type': 'application/json' },
|
||
|
|
body: JSON.stringify(body)
|
||
|
|
});
|
||
|
|
const d = await r.json();
|
||
|
|
if (!r.ok) throw new Error(d.error?.message || 'Perplexity-fel');
|
||
|
|
|
||
|
|
const text = d.choices?.[0]?.message?.content || '';
|
||
|
|
const citations = d.citations || [];
|
||
|
|
return { text, citations };
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── AWS Nova Pro — långt kontextfönster (300k tokens) ─────────────────────────
|
||
|
|
export async function novaPro(systemPrompt, messages, maxTokens = 4096) {
|
||
|
|
const body = JSON.stringify({
|
||
|
|
messages: [
|
||
|
|
...(systemPrompt ? [{ role: 'user', content: [{ text: `[SYSTEM]\n${systemPrompt}` }] }] : []),
|
||
|
|
...messages.map(m => ({ role: m.role === 'assistant' ? 'assistant' : 'user', content: [{ text: typeof m.content === 'string' ? m.content : extractText(m.content) }] }))
|
||
|
|
],
|
||
|
|
inferenceConfig: { max_new_tokens: maxTokens, temperature: 0.7 }
|
||
|
|
});
|
||
|
|
|
||
|
|
const cmd = new InvokeModelCommand({
|
||
|
|
modelId: 'amazon.nova-pro-v1:0',
|
||
|
|
body: Buffer.from(body),
|
||
|
|
contentType: 'application/json',
|
||
|
|
accept: 'application/json'
|
||
|
|
});
|
||
|
|
|
||
|
|
const response = await bedrock.send(cmd);
|
||
|
|
const result = JSON.parse(Buffer.from(response.body).toString());
|
||
|
|
return result.output?.message?.content?.[0]?.text || '';
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── DeepSeek-R2 via Bedrock (reasoning) ──────────────────────────────────────
|
||
|
|
export async function deepseekReason(systemPrompt, messages, maxTokens = 4096) {
|
||
|
|
// DeepSeek-R1 är tillgänglig via Bedrock
|
||
|
|
const body = JSON.stringify({
|
||
|
|
messages: [
|
||
|
|
...(systemPrompt ? [{ role: 'system', content: systemPrompt }] : []),
|
||
|
|
...messages.map(m => ({ role: m.role, content: typeof m.content === 'string' ? m.content : extractText(m.content) }))
|
||
|
|
],
|
||
|
|
max_tokens: maxTokens,
|
||
|
|
temperature: 0.6
|
||
|
|
});
|
||
|
|
|
||
|
|
const cmd = new InvokeModelCommand({
|
||
|
|
modelId: 'deepseek.r1-v1:0',
|
||
|
|
body: Buffer.from(body),
|
||
|
|
contentType: 'application/json',
|
||
|
|
accept: 'application/json'
|
||
|
|
});
|
||
|
|
|
||
|
|
try {
|
||
|
|
const response = await bedrock.send(cmd);
|
||
|
|
const result = JSON.parse(Buffer.from(response.body).toString());
|
||
|
|
return result.choices?.[0]?.message?.content || result.output || '';
|
||
|
|
} catch(e) {
|
||
|
|
// Fallback till Qwen om DeepSeek ej tillgänglig i region
|
||
|
|
return qwenCreate(MODELS.QWEN_235B, systemPrompt, messages, maxTokens);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
export async function claudeChat(model, system, messages, options = {}) {
|
||
|
|
const { maxTokens = 4096, enableCaching = true, enableThinking = true, thinkingBudget = 8000 } = options;
|
||
|
|
const Anthropic = (await import('@anthropic-ai/sdk')).default;
|
||
|
|
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
|
||
|
|
let systemParam = system;
|
||
|
|
if (enableCaching && system && system.length > 1024) {
|
||
|
|
systemParam = [{ type: 'text', text: system, cache_control: { type: 'ephemeral' } }];
|
||
|
|
}
|
||
|
|
const params = { model, max_tokens: maxTokens, system: systemParam, messages };
|
||
|
|
if (enableThinking && (model.includes('opus-4-5') || model.includes('sonnet-4-5'))) {
|
||
|
|
params.thinking = { type: 'enabled', budget_tokens: thinkingBudget };
|
||
|
|
}
|
||
|
|
const resp = await client.messages.create(params);
|
||
|
|
return { text: resp.content.find(c => c.type === 'text')?.text || '', usage: resp.usage, model: resp.model };
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function gptChat(model, system, messages) {
|
||
|
|
const r = await fetch('https://api.openai.com/v1/chat/completions', {
|
||
|
|
method: 'POST',
|
||
|
|
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${process.env.OPENAI_API_KEY}` },
|
||
|
|
body: JSON.stringify({ model: model||'gpt-4o', max_tokens: 1024, messages: [{ role:'system', content: system }, ...messages] }),
|
||
|
|
signal: AbortSignal.timeout(30000)
|
||
|
|
});
|
||
|
|
const d = await r.json();
|
||
|
|
return { text: d.choices?.[0]?.message?.content || '', model };
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function groqChat(model, system, messages) {
|
||
|
|
const r = await fetch('https://api.groq.com/openai/v1/chat/completions', {
|
||
|
|
method: 'POST',
|
||
|
|
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${process.env.GROQ_API_KEY}` },
|
||
|
|
body: JSON.stringify({ model: model||'llama-3.3-70b-versatile', max_tokens: 1024, messages: [{ role:'system', content: system }, ...messages] }),
|
||
|
|
signal: AbortSignal.timeout(15000)
|
||
|
|
});
|
||
|
|
const d = await r.json();
|
||
|
|
return { text: d.choices?.[0]?.message?.content || '', model };
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
// ── Ollama — local inference (qwen2.5:7b, CPU) ─────────────────────────────
|
||
|
|
export async function ollamaChat(model = 'qwen2.5:7b', system, messages, maxTokens = 512) {
|
||
|
|
const msgs = [...(system ? [{ role: 'system', content: system }] : []), ...messages];
|
||
|
|
const r = await fetch('http://localhost:11434/api/chat', {
|
||
|
|
method: 'POST',
|
||
|
|
headers: { 'Content-Type': 'application/json' },
|
||
|
|
body: JSON.stringify({ model, messages: msgs, stream: false, options: { num_predict: maxTokens } }),
|
||
|
|
signal: AbortSignal.timeout(120000)
|
||
|
|
});
|
||
|
|
const d = await r.json();
|
||
|
|
if (!r.ok) throw new Error(`ollama ${r.status}: ${d.error || ''}`);
|
||
|
|
return { text: d.message?.content || '', model };
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Aletheia v6 — ESLM via Bedrock custom model (2026-06-07) ─────────────
|
||
|
|
// Importerad i us-east-1 — kräver egen Bedrock-klient för rätt region.
|
||
|
|
const _aletheiaClient = new BedrockRuntimeClient({ region: 'us-east-1' });
|
||
|
|
export async function aletheiaCreate(systemPrompt, messages, maxTokens = 1024) {
|
||
|
|
return _wrapLLM(async () => {
|
||
|
|
const body = JSON.stringify({
|
||
|
|
messages: [
|
||
|
|
...(systemPrompt ? [{ role: 'system', content: systemPrompt }] : []),
|
||
|
|
...messages.map(m => ({ role: m.role, content: typeof m.content === 'string' ? m.content : extractText(m.content) }))
|
||
|
|
],
|
||
|
|
max_tokens: maxTokens,
|
||
|
|
temperature: 0.1
|
||
|
|
});
|
||
|
|
const cmd = new InvokeModelCommand({
|
||
|
|
modelId: MODELS.ALETHEIA_V6,
|
||
|
|
body: Buffer.from(body),
|
||
|
|
contentType: 'application/json',
|
||
|
|
accept: 'application/json'
|
||
|
|
});
|
||
|
|
const response = await _aletheiaClient.send(cmd);
|
||
|
|
const result = JSON.parse(Buffer.from(response.body).toString());
|
||
|
|
const text = result?.choices?.[0]?.message?.content || '';
|
||
|
|
return { text, model: 'aletheia-v6' };
|
||
|
|
}, 'aletheia-v6', messages);
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function intelligenceRouter(task, message, system, messages = []) {
|
||
|
|
const opts = { enableCaching: true, enableThinking: true };
|
||
|
|
const handlers = {
|
||
|
|
'fast': () => groqChat('llama-3.3-70b-versatile', system, messages),
|
||
|
|
'reasoning': () => claudeChat('claude-opus-4-5', system, messages, { ...opts, thinkingBudget: 16000 }),
|
||
|
|
'search': () => perplexitySearch(message),
|
||
|
|
'code': () => qwenCreate(MODELS.QWEN_CODER, system, messages, 4096),
|
||
|
|
'financial': () => claudeChat('claude-sonnet-4-5', system, messages, { ...opts, thinkingBudget: 12000 }),
|
||
|
|
'creative': () => gptChat('gpt-4o', system, messages),
|
||
|
|
'analysis': () => claudeChat('claude-opus-4-5', system, messages, { ...opts, thinkingBudget: 16000 }),
|
||
|
|
// Hermes-slot: OpenAI OSS 120B via Groq — agentic/skill/self-improvement tasks
|
||
|
|
'hermes': () => groqChat('openai/gpt-oss-120b', system, messages),
|
||
|
|
'agent': () => groqChat('openai/gpt-oss-120b', system, messages),
|
||
|
|
'skill': () => groqChat('openai/gpt-oss-120b', system, messages),
|
||
|
|
'self-improve': () => groqChat('openai/gpt-oss-120b', system, messages),
|
||
|
|
// Aletheia v6 — ESLM för mailtriage/klassning (Bedrock custom, 2026-06-07)
|
||
|
|
// 100% red-team pass (Opus 4.8 domare). Körs ~gratis vs Sonnet.
|
||
|
|
'triage': () => aletheiaCreate(system, messages, 1024),
|
||
|
|
'classify': () => aletheiaCreate(system, messages, 512),
|
||
|
|
'default': () => claudeChat('claude-sonnet-4-5', system, messages, opts),
|
||
|
|
};
|
||
|
|
try {
|
||
|
|
let result = await (handlers[task] || handlers['default'])();
|
||
|
|
if (typeof result === 'string') result = { text: result };
|
||
|
|
return { ...result, source: task };
|
||
|
|
} catch(e) {
|
||
|
|
return { text: 'Tillfälligt fel: ' + e.message, error: true };
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
// -- routeQuery: Smart model router (2026-05-20) --------------------------
|
||
|
|
// Selects provider/model based on query characteristics.
|
||
|
|
export function routeQuery(question, options = {}) {
|
||
|
|
const q = question.toLowerCase();
|
||
|
|
|
||
|
|
// Short/simple query -> local Ollama (gratis, CPU)
|
||
|
|
if (question.length < 120 && !options.forceExternal) {
|
||
|
|
return { model: 'qwen2.5:7b', provider: 'ollama', reason: 'short_query_local' };
|
||
|
|
}
|
||
|
|
|
||
|
|
// Juridik/compliance -> DeepSeek
|
||
|
|
if (/juridik|lag|gdpr|compliance|avtal|kontrakt/i.test(q)) {
|
||
|
|
return { model: 'deepseek-chat', provider: 'deepseek', reason: 'legal_domain' };
|
||
|
|
}
|
||
|
|
|
||
|
|
// Kod/teknik -> Groq
|
||
|
|
if (/kod|programmering|typescript|python|javascript|sql|docker/i.test(q)) {
|
||
|
|
return { model: 'llama-3.3-70b-versatile', provider: 'groq', reason: 'technical_domain' };
|
||
|
|
}
|
||
|
|
|
||
|
|
// Default -> Groq
|
||
|
|
return { model: 'llama-3.3-70b-versatile', provider: 'groq', reason: 'default' };
|
||
|
|
}
|