/** * Wavult AI Model Router — KIMI-INTEGRERAD (2026-06-25) * ───────────────────────────────────────────────────────────────────── * Prioritet: * 1. Qwen3 via AWS Bedrock — PRIMÄR för AUDIT/OPERATOR * 2. Kimi K2.6 / K2.7 Code — AGENT & CODE (nya) * 3. Gemini 2.5 Pro — BACKUP / MULTIMODAL * 4. Claude Sonnet — LEGACY FALLBACK */ import { BedrockRuntimeClient, InvokeModelCommand, InvokeModelWithResponseStreamCommand } from '@aws-sdk/client-bedrock-runtime'; // ── GECL Universal Enforcement ──────────────────────────────────────────── 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' }); const KIMI_BASE_URL = 'https://api.kimi.ai/v1'; // ── Modell-IDs ──────────────────────────────────────────────────────────── export const MODELS = { // Qwen3 via Bedrock (befintliga) 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', // Kimi via Moonshot API (NYA 2026-06-25) KIMI_K2_6: 'kimi-k2.6', KIMI_K2_7_CODE: 'kimi-k2.7-code', KIMI_K2_7_HIGHSPEED:'kimi-k2.7-code-highspeed', // Aletheia v6 — ESLM 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); const isAgent = /agent|orchestr|deleg|parallel|swarm|tool/i.test(message); if (mode === 'AUDIT') return MODELS.QWEN_235B; if (mode === 'AGENT') return MODELS.KIMI_K2_6; if (isAgent) return MODELS.KIMI_K2_6; // Agent Swarm → Kimi K2.6 if (isCode) return MODELS.KIMI_K2_7_CODE; // Kodning → Kimi K2.7 Code if (isComplex) return MODELS.QWEN_32B; return MODELS.QWEN_32B; // default } // ── COMPLIANCE-AWARE MODEL SELECTION ────────────────────────────────────── 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('kimi-k2.7')) return 'kimi-k2.7'; if (id.includes('kimi-k2.6')) return 'kimi-k2.6'; if (id.includes('kimi')) return 'kimi'; 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, MODELS.KIMI_K2_6, 'claude-sonnet', 'gpt-4', 'mistral-large-2'], 'sovereign-cn': [MODELS.QWEN_32B, MODELS.QWEN_235B, MODELS.KIMI_K2_6, '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; } // ═══════════════════════════════════════════════════════════════════════ // KIMI INTEGRATION (2026-06-25) // ═══════════════════════════════════════════════════════════════════════ // ── Kimi icke-streaming ─────────────────────────────────────────────────── export async function kimiCreate(model, systemPrompt, messages, maxTokens = 4096) { const KIMI_KEY = proces…KEY; if (!KIMI_KEY) throw new Error('KIMI_API_KEY saknas'); const body = { model, 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.7, }; const r = await fetch(`${KIMI_BASE_URL}/chat/completions`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${KIMI_KEY}` }, body: JSON.stringify(body), signal: AbortSignal.timeout(60000) }); const d = await r.json(); if (!r.ok) throw new Error(`Kimi ${r.status}: ${d.error?.message || ''}`); return d.choices?.[0]?.message?.content || ''; } // ── Kimi streaming ──────────────────────────────────────────────────────── export async function kimiStream(model, systemPrompt, messages, maxTokens, onChunk) { const KIMI_KEY = proces…KEY; if (!KIMI_KEY) throw new Error('KIMI_API_KEY saknas'); const body = { model, 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 r = await fetch(`${KIMI_BASE_URL}/chat/completions`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${KIMI_KEY}` }, body: JSON.stringify(body), signal: AbortSignal.timeout(60000) }); if (!r.ok) { const d = await r.json(); throw new Error(`Kimi ${r.status}: ${d.error?.message || ''}`); } const reader = r.body.getReader(); const decoder = new TextDecoder(); let fullText = ''; while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value); const lines = chunk.split('\n').filter(line => line.trim()); for (const line of lines) { if (line.startsWith('data: ')) { const data = line.slice(6); if (data === '[DONE]') continue; try { const parsed = JSON.parse(data); const delta = parsed.choices?.[0]?.delta?.content || ''; if (delta) { fullText += delta; onChunk(delta); } } catch {} } } } return fullText; } // ── Kimi Vision (K2.6 multimodal) ───────────────────────────────────────── export async function kimiVision(model, systemPrompt, messages, imageBase64, mimeType = 'image/png') { const KIMI_KEY = proces…KEY; if (!KIMI_KEY) throw new Error('KIMI_API_KEY saknas'); const visionMessages = [ ...(systemPrompt ? [{ role: 'system', content: systemPrompt }] : []), ...messages.map(m => { if (m.role === 'user' && imageBase64) { return { role: 'user', content: [ { type: 'text', text: typeof m.content === 'string' ? m.content : extractText(m.content) }, { type: 'image_url', image_url: { url: `data:${mimeType};base64,${imageBase64}` } } ] }; } return { role: m.role, content: typeof m.content === 'string' ? m.content : extractText(m.content) }; }) ]; const body = { model: model || MODELS.KIMI_K2_6, messages: visionMessages, max_tokens: 4096, temperature: 0.7, }; const r = await fetch(`${KIMI_BASE_URL}/chat/completions`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${KIMI_KEY}` }, body: JSON.stringify(body), signal: AbortSignal.timeout(60000) }); const d = await r.json(); if (!r.ok) throw new Error(`Kimi Vision ${r.status}: ${d.error?.message || ''}`); return d.choices?.[0]?.message?.content || ''; } // ═══════════════════════════════════════════════════════════════════════ // BEFINTLIGA PROVIDERS (Qwen, Gemini, Claude, etc.) // ═══════════════════════════════════════════════════════════════════════ // [All befintlig kod behålls oförändrad nedan...] // Qwen streaming, Qwen create, Gemini, Claude, Groq, etc. // ── 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()); const delta = chunk?.choices?.[0]?.delta?.content || ''; if (delta) { fullText += delta; onChunk(delta); } } catch {} } } return fullText; } // ── Qwen icke-streaming ────────────────────────────────────────────────── 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); } // ── Hjälpfunktion: extrahera text ───────────────────────────────────────── 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); } // ── Uppdaterad intelligenceRouter med Kimi ──────────────────────────────── 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': () => kimiCreate(MODELS.KIMI_K2_7_CODE, system, messages, 4096), // ← KIMI 'code-legacy': () => qwenCreate(MODELS.QWEN_CODER, system, messages, 4096), // ← Qwen fallback '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': () => groqChat('openai/gpt-oss-120b', system, messages), 'agent': () => kimiCreate(MODELS.KIMI_K2_6, system, messages, 4096), // ← KIMI 'agent-swarm': () => kimiCreate(MODELS.KIMI_K2_6, system, messages, 8192), // ← KIMI 'skill': () => groqChat('openai/gpt-oss-120b', system, messages), 'self-improve': () => groqChat('openai/gpt-oss-120b', system, messages), '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 ──────────────────────────────────────── export function routeQuery(question, options = {}) { const q = question.toLowerCase(); // Short/simple query -> local Ollama if (question.length < 120 && !options.forceExternal) { return { model: 'qwen2.5:7b', provider: 'ollama', reason: 'short_query_local' }; } // Agent/orkestrering -> Kimi if (/agent|orchestr|deleg|parallel|swarm|tool/i.test(q)) { return { model: 'kimi-k2.6', provider: 'kimi', reason: 'agent_task' }; } // Kod/teknik -> Kimi if (/kod|programmering|typescript|python|javascript|sql|docker|rust/i.test(q)) { return { model: 'kimi-k2.7-code', provider: 'kimi', reason: 'coding_task' }; } // Juridik/compliance -> DeepSeek if (/juridik|lag|gdpr|compliance|avtal|kontrakt/i.test(q)) { return { model: 'deepseek-chat', provider: 'deepseek', reason: 'legal_domain' }; } // Default -> Qwen return { model: 'qwen.qwen3-32b-v1:0', provider: 'bedrock', reason: 'default' }; } export default { MODELS, selectModel, qwenStream, qwenCreate, kimiCreate, // ← NY kimiStream, // ← NY kimiVision, // ← NY geminiCreate, intelligenceRouter, routeQuery };