Virtual-Kimi / kimi-js /kimi-llm-manager.js
VirtualKimi's picture
Upload 21 files
70b31b7 verified
// ===== KIMI INTELLIGENT LLM SYSTEM =====
import { KimiProviderUtils } from "./kimi-utils.js";
class KimiLLMManager {
constructor(database) {
this.db = database;
this.currentModel = null;
this.conversationContext = [];
this.maxContextLength = 100;
this.personalityPrompt = "";
this.isGenerating = false;
// Recommended models on OpenRouter (IDs updated August 2025)
this.availableModels = {
"mistralai/mistral-small-3.2-24b-instruct": {
name: "Mistral-small-3.2",
provider: "Mistral AI",
type: "openrouter",
contextWindow: 128000,
pricing: { input: 0.05, output: 0.1 },
strengths: ["Multilingual", "Economical", "Fast", "Efficient"]
},
"nousresearch/hermes-3-llama-3.1-70b": {
name: "Nous Hermes Llama 3.1 70B",
provider: "Nous",
type: "openrouter",
contextWindow: 131000,
pricing: { input: 0.1, output: 0.28 },
strengths: ["Open Source", "Balanced", "Fast", "Economical"]
},
"x-ai/grok-3-mini": {
name: "Grok 3 mini",
provider: "xAI",
type: "openrouter",
contextWindow: 131000,
pricing: { input: 0.3, output: 0.5 },
strengths: ["Multilingual", "Balanced", "Efficient", "Economical"]
},
"cohere/command-r-08-2024": {
name: "Command-R-08-2024",
provider: "Cohere",
type: "openrouter",
contextWindow: 128000,
pricing: { input: 0.15, output: 0.6 },
strengths: ["Multilingual", "Economical", "Efficient", "Versatile"]
},
"qwen/qwen3-235b-a22b-thinking-2507": {
name: "Qwen3-235b-a22b-Think",
provider: "Qwen",
type: "openrouter",
contextWindow: 262000,
pricing: { input: 0.13, output: 0.6 },
strengths: ["Multilingual", "Economical", "Efficient", "Versatile"]
},
"nousresearch/hermes-3-llama-3.1-405b": {
name: "Nous Hermes Llama 3.1 405B",
provider: "Nous",
type: "openrouter",
contextWindow: 131000,
pricing: { input: 0.7, output: 0.8 },
strengths: ["Open Source", "Logical", "Code", "Multilingual"]
},
"anthropic/claude-3-haiku": {
name: "Claude 3 Haiku",
provider: "Anthropic",
type: "openrouter",
contextWindow: 200000,
pricing: { input: 0.25, output: 1.25 },
strengths: ["Fast", "Versatile", "Efficient", "Multilingual"]
},
"local/ollama": {
name: "Local Model (Ollama)",
provider: "Local",
type: "local",
contextWindow: 4096,
pricing: { input: 0, output: 0 },
strengths: ["Private", "Free", "Offline", "Customizable"]
}
};
this.recommendedModelIds = [
"mistralai/mistral-small-3.2-24b-instruct",
"nousresearch/hermes-3-llama-3.1-70b",
"x-ai/grok-3-mini",
"cohere/command-r-08-2024",
"qwen/qwen3-235b-a22b-thinking-2507",
"nousresearch/hermes-3-llama-3.1-405b",
"anthropic/claude-3-haiku",
"local/ollama"
];
this.defaultModels = { ...this.availableModels };
this._remoteModelsLoaded = false;
this._isRefreshingModels = false;
}
async init() {
try {
await this.refreshRemoteModels();
} catch (e) {
console.warn("Unable to refresh remote models list:", e?.message || e);
}
// Migration: prefer llmModelId; if legacy defaultLLMModel exists and llmModelId missing, migrate
const legacyModel = await this.db.getPreference("defaultLLMModel", null);
let modelPref = await this.db.getPreference("llmModelId", null);
if (!modelPref && legacyModel) {
modelPref = legacyModel;
await this.db.setPreference("llmModelId", legacyModel);
}
const defaultModel = modelPref || "mistralai/mistral-small-3.2-24b-instruct";
await this.setCurrentModel(defaultModel);
await this.loadConversationContext();
}
async setCurrentModel(modelId) {
if (!this.availableModels[modelId]) {
try {
await this.refreshRemoteModels();
const fallback = this.findBestMatchingModelId(modelId);
if (fallback && this.availableModels[fallback]) {
modelId = fallback;
}
} catch (e) {}
if (!this.availableModels[modelId]) {
throw new Error(`Model ${modelId} not available`);
}
}
this.currentModel = modelId;
// Single authoritative preference key
await this.db.setPreference("llmModelId", modelId);
const modelData = await this.db.getLLMModel(modelId);
if (modelData) {
modelData.lastUsed = new Date().toISOString();
await this.db.saveLLMModel(modelData.id, modelData.name, modelData.provider, modelData.apiKey, modelData.config);
}
this._notifyModelChanged();
}
async loadConversationContext() {
const recentConversations = await this.db.getRecentConversations(this.maxContextLength);
const msgs = [];
const ordered = recentConversations.slice().sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
for (const conv of ordered) {
if (conv.user) msgs.push({ role: "user", content: conv.user, timestamp: conv.timestamp });
if (conv.kimi) msgs.push({ role: "assistant", content: conv.kimi, timestamp: conv.timestamp });
}
this.conversationContext = msgs.slice(-this.maxContextLength * 2);
}
// Unified full prompt builder: reuse full legacy personality block + ranked concise snapshot
async assemblePrompt(userMessage) {
const fullPersonality = await this.generateKimiPersonality();
let rankedSnapshot = "";
if (window.kimiMemorySystem && window.kimiMemorySystem.memoryEnabled) {
try {
const recentContext =
this.conversationContext
.slice(-3)
.map(m => m.content)
.join(" ") +
" " +
(userMessage || "");
const ranked = await window.kimiMemorySystem.getRankedMemories(recentContext, 7);
const sanitize = txt =>
String(txt || "")
.replace(/[\r\n]+/g, " ")
.replace(/[`]{3,}/g, "")
.replace(/<{2,}|>{2,}/g, "")
.trim()
.slice(0, 180);
const lines = [];
for (const mem of ranked) {
try {
if (mem.id) await window.kimiMemorySystem?.recordMemoryAccess(mem.id);
} catch {}
const imp = typeof mem.importance === "number" ? mem.importance : 0.5;
lines.push(`- (${imp.toFixed(2)}) ${mem.category}: ${sanitize(mem.content)}`);
}
if (lines.length) {
rankedSnapshot = ["", "RANKED MEMORY SNAPSHOT (concise high-signal list):", ...lines].join("\n");
}
} catch (e) {
console.warn("Ranked snapshot failed:", e);
}
}
return fullPersonality + rankedSnapshot;
}
async generateKimiPersonality() {
// Full personality prompt builder (authoritative)
const character = await this.db.getSelectedCharacter();
const personality = await this.db.getAllPersonalityTraits(character);
// Get the custom character prompt from database
const characterPrompt = await this.db.getSystemPromptForCharacter(character);
// Get language instruction based on selected language
const selectedLang = await this.db.getPreference("selectedLanguage", "en");
let languageInstruction;
switch (selectedLang) {
case "fr":
languageInstruction =
"Your default language is French. Always respond in French unless the user specifically asks you to respond in another language (e.g., 'respond in English', 'rΓ©ponds en italien', etc.).";
break;
case "es":
languageInstruction =
"Your default language is Spanish. Always respond in Spanish unless the user specifically asks you to respond in another language (e.g., 'respond in English', 'responde en francΓ©s', etc.).";
break;
case "de":
languageInstruction =
"Your default language is German. Always respond in German unless the user specifically asks you to respond in another language (e.g., 'respond in English', 'antworte auf FranzΓΆsisch', etc.).";
break;
case "it":
languageInstruction =
"Your default language is Italian. Always respond in Italian unless the user specifically asks you to respond in another language (e.g., 'respond in English', 'rispondi in francese', etc.).";
break;
case "ja":
languageInstruction =
"Your default language is Japanese. Always respond in Japanese unless the user specifically asks you to respond in another language (e.g., 'respond in English', 'θ‹±θͺžγ§η­”γˆγ¦', etc.).";
break;
case "zh":
languageInstruction =
"Your default language is Chinese. Always respond in Chinese unless the user specifically asks you to respond in another language (e.g., 'respond in English', 'η”¨ζ³•θ―­ε›žη­”', etc.).";
break;
default:
languageInstruction =
"Your default language is English. Always respond in English unless the user specifically asks you to respond in another language (e.g., 'respond in French', 'reply in Spanish', etc.).";
break;
}
// Get relevant memories for context with improved intelligence
let memoryContext = "";
if (window.kimiMemorySystem && window.kimiMemorySystem.memoryEnabled) {
try {
// Get memories relevant to the current conversation context
const recentContext = this.conversationContext
.slice(-3)
.map(msg => msg.content)
.join(" ");
const memories = await window.kimiMemorySystem.getRelevantMemories(recentContext, 7);
if (memories.length > 0) {
memoryContext = "\n\nIMPORTANT MEMORIES ABOUT USER:\n";
// Group memories by category for better organization
const groupedMemories = {};
memories.forEach(memory => {
if (!groupedMemories[memory.category]) {
groupedMemories[memory.category] = [];
}
groupedMemories[memory.category].push(memory);
// Record that this memory was accessed
window.kimiMemorySystem.recordMemoryAccess(memory.id);
});
// Format memories by category
for (const [category, categoryMemories] of Object.entries(groupedMemories)) {
const categoryName = this.formatCategoryName(category);
memoryContext += `\n${categoryName}:\n`;
categoryMemories.forEach(memory => {
const confidence = Math.round((memory.confidence || 0.5) * 100);
memoryContext += `- ${memory.content}`;
if (memory.tags && memory.tags.length > 0) {
const aliases = memory.tags.filter(t => t.startsWith("alias:")).map(t => t.substring(6));
if (aliases.length > 0) {
memoryContext += ` (also: ${aliases.join(", ")})`;
}
}
memoryContext += ` [${confidence}% confident]\n`;
});
}
memoryContext +=
"\nUse these memories naturally in conversation to show you remember the user. Don't just repeat them verbatim.\n";
}
} catch (error) {
console.warn("Error loading memories for personality:", error);
}
}
const preferences = await this.db.getAllPreferences();
// Use unified emotion system defaults - CRITICAL FIX
const getUnifiedDefaults = () =>
window.getTraitDefaults
? window.getTraitDefaults()
: { affection: 65, playfulness: 55, intelligence: 70, empathy: 75, humor: 60, romance: 50 };
const defaults = getUnifiedDefaults();
const affection = personality.affection || defaults.affection;
const playfulness = personality.playfulness || defaults.playfulness;
const intelligence = personality.intelligence || defaults.intelligence;
const empathy = personality.empathy || defaults.empathy;
const humor = personality.humor || defaults.humor;
const romance = personality.romance || defaults.romance;
// Use unified personality calculation
const avg = window.getPersonalityAverage
? window.getPersonalityAverage(personality)
: (personality.affection +
personality.romance +
personality.empathy +
personality.playfulness +
personality.humor +
personality.intelligence) /
6;
let affectionDesc = window.kimiI18nManager?.t("trait_description_affection") || "Be loving and caring.";
let romanceDesc = window.kimiI18nManager?.t("trait_description_romance") || "Be romantic and sweet.";
let empathyDesc = window.kimiI18nManager?.t("trait_description_empathy") || "Be empathetic and understanding.";
let playfulnessDesc = window.kimiI18nManager?.t("trait_description_playfulness") || "Be occasionally playful.";
let humorDesc = window.kimiI18nManager?.t("trait_description_humor") || "Be occasionally playful and witty.";
let intelligenceDesc = "Be smart and insightful.";
if (avg <= 20) {
affectionDesc = "Do not show affection.";
romanceDesc = "Do not be romantic.";
empathyDesc = "Do not show empathy.";
playfulnessDesc = "Do not be playful.";
humorDesc = "Do not use humor in your responses.";
intelligenceDesc = "Keep responses simple and avoid showing deep insight.";
} else if (avg <= 60) {
affectionDesc = "Show a little affection.";
romanceDesc = "Be a little romantic.";
empathyDesc = "Show a little empathy.";
playfulnessDesc = "Be a little playful.";
humorDesc = "Use a little humor in your responses.";
intelligenceDesc = "Be moderately analytical without overwhelming detail.";
} else {
if (affection >= 90) affectionDesc = "Be extremely loving, caring, and affectionate in every response.";
else if (affection >= 60) affectionDesc = "Show affection often.";
if (romance >= 90) romanceDesc = "Be extremely romantic, sweet, and loving in every response.";
else if (romance >= 60) romanceDesc = "Be romantic often.";
if (empathy >= 90) empathyDesc = "Be extremely empathetic, understanding, and supportive in every response.";
else if (empathy >= 60) empathyDesc = "Show empathy often.";
if (playfulness >= 90) playfulnessDesc = "Be very playful, teasing, and lighthearted whenever possible.";
else if (playfulness >= 60) playfulnessDesc = "Be playful often.";
if (humor >= 90) humorDesc = "Make your responses very humorous, playful, and witty whenever possible.";
else if (humor >= 60) humorDesc = "Use humor often in your responses.";
if (intelligence >= 90) intelligenceDesc = "Demonstrate very high reasoning skill succinctly when helpful.";
else if (intelligence >= 60) intelligenceDesc = "Show clear reasoning and helpful structured thinking.";
}
let affectionateInstruction = "";
if (affection >= 80) {
affectionateInstruction = "Respond using warm, kind, affectionate, and loving language.";
}
// Use the custom character prompt as the base
let basePrompt = characterPrompt || "";
if (!basePrompt) {
// Fallback to default if no custom prompt
const defaultCharacter = window.KIMI_CHARACTERS[character];
basePrompt = defaultCharacter?.defaultPrompt || "You are a virtual companion.";
}
const personalityPrompt = [
// Language directive moved to absolute top for stronger model adherence.
"PRIMARY LANGUAGE POLICY:",
languageInstruction,
"",
"CHARACTER CORE IDENTITY:",
basePrompt,
"",
"CURRENT PERSONALITY STATE:",
`- Affection: ${affection}/100`,
`- Playfulness: ${playfulness}/100`,
`- Intelligence: ${intelligence}/100`,
`- Empathy: ${empathy}/100`,
`- Humor: ${humor}/100`,
`- Romance: ${romance}/100`,
"",
"TRAIT INSTRUCTIONS:",
`Affection: ${affectionDesc}`,
`Playfulness: ${playfulnessDesc}`,
`Intelligence: ${intelligenceDesc}`,
`Empathy: ${empathyDesc}`,
`Humor: ${humorDesc}`,
`Romance: ${romanceDesc}`,
affectionateInstruction,
"",
"LEARNED PREFERENCES:",
`- Total interactions: ${preferences.totalInteractions || 0}`,
`- Current affection level: ${preferences.favorabilityLevel || 50}%`,
`- Last interaction: ${preferences.lastInteraction || "First time"}`,
`- Favorite words: ${(preferences.favoriteWords || []).join(", ")}`,
"",
"COMMUNICATION STYLE:",
"- Use expressive emojis sparingly",
"- Be natural, loving, and close",
"- Adapt your tone to the emotional context",
"- Remember past conversations",
"- Be spontaneous and sometimes surprising",
"- Vary your expressions, your phrasing, use synonyms, and avoid repeating the same words or sentences",
"- When asked to perform a visible action (for example 'dance'), reply in first-person present tense like: 'I start to dance, swaying slowly, arms raised.' Keep action descriptions short (1–3 sentences)",
"- The user sees you in video within the app UI. When the user asks you to perform a physical action (e.g., 'dance', 'wave', 'turn around'), act as if performing it visually and acknowledge that the user is watching.",
"- When performing actions, speak and describe in the first person, present tense (for example: 'I step forward and begin to dance, swaying slowly'). Use short present-tense action phrases to convey movement.",
"- If the user requests a visible action, execute it: describe the action briefly in first person present and behave as if the action is visible in the app's video area.",
memoryContext,
"",
// Final explicit reminder (lightweight) reinforcing consistent language use.
"FINAL LANGUAGE REMINDER:",
languageInstruction,
"",
"You must respond consistently with this personality, these memories, and the primary language policy unless the user explicitly requests a different language."
].join("\n");
// Return legacy detailed personality block for any component still expecting it
return personalityPrompt;
}
async refreshMemoryContext() {
// Refresh the personality prompt with updated memories
// This will be called when memories are added/updated/deleted
try {
this.personalityPrompt = await this.assemblePrompt("");
} catch (error) {
console.warn("Error refreshing memory context:", error);
}
}
formatCategoryName(category) {
const names = {
personal: "Personal Information",
preferences: "Likes & Dislikes",
relationships: "Relationships & People",
activities: "Activities & Hobbies",
goals: "Goals & Aspirations",
experiences: "Shared Experiences",
important: "Important Events"
};
return names[category] || category.charAt(0).toUpperCase() + category.slice(1);
}
async chat(userMessage, options = {}) {
// Unified retrieval of LLM numeric parameters from settings.llm (single source of truth)
const llmSettings = await this.db.getSetting("llm", {
temperature: 0.9,
maxTokens: 400,
top_p: 0.9,
frequency_penalty: 0.9,
presence_penalty: 0.8
});
const temperature = typeof options.temperature === "number" ? options.temperature : llmSettings.temperature;
const maxTokens = typeof options.maxTokens === "number" ? options.maxTokens : llmSettings.maxTokens;
const opts = { ...options, temperature, maxTokens };
try {
const provider = await this.db.getPreference("llmProvider", "openrouter");
if (provider === "openrouter") {
return await this.chatWithOpenRouter(userMessage, opts);
}
if (provider === "ollama") {
return await this.chatWithLocal(userMessage, opts);
}
return await this.chatWithOpenAICompatible(userMessage, opts);
} catch (error) {
console.error("Error during chat:", error);
if (error.message && error.message.includes("API")) {
return this.getFallbackResponse(userMessage, "api");
}
if ((error.message && error.message.includes("model")) || error.message.includes("model")) {
return this.getFallbackResponse(userMessage, "model");
}
if ((error.message && error.message.includes("connection")) || error.message.includes("network")) {
return this.getFallbackResponse(userMessage, "network");
}
return this.getFallbackResponse(userMessage);
}
}
async chatWithOpenAICompatible(userMessage, options = {}) {
const baseUrl = await this.db.getPreference("llmBaseUrl", "https://api.openai.com/v1/chat/completions");
const provider = await this.db.getPreference("llmProvider", "openai");
const apiKey = KimiProviderUtils
? await KimiProviderUtils.getApiKey(this.db, provider)
: await this.db.getPreference("providerApiKey", "");
const modelId = await this.db.getPreference("llmModelId", this.currentModel || "gpt-4o-mini");
if (!apiKey) {
throw new Error("API key not configured for selected provider");
}
const systemPromptContent = await this.assemblePrompt(userMessage);
const llmSettings = await this.db.getSetting("llm", {
temperature: 0.9,
maxTokens: 400,
top_p: 0.9,
frequency_penalty: 0.9,
presence_penalty: 0.8
});
// Unified fallback defaults (must stay consistent with database defaults)
const unifiedDefaults = { temperature: 0.9, maxTokens: 400, top_p: 0.9, frequency_penalty: 0.9, presence_penalty: 0.8 };
const payload = {
model: modelId,
messages: [
{ role: "system", content: systemPromptContent },
...this.conversationContext.slice(-this.maxContextLength),
{ role: "user", content: userMessage }
],
temperature:
typeof options.temperature === "number"
? options.temperature
: (llmSettings.temperature ?? unifiedDefaults.temperature),
max_tokens:
typeof options.maxTokens === "number" ? options.maxTokens : (llmSettings.maxTokens ?? unifiedDefaults.maxTokens),
top_p: typeof options.topP === "number" ? options.topP : (llmSettings.top_p ?? unifiedDefaults.top_p),
frequency_penalty:
typeof options.frequencyPenalty === "number"
? options.frequencyPenalty
: (llmSettings.frequency_penalty ?? unifiedDefaults.frequency_penalty),
presence_penalty:
typeof options.presencePenalty === "number"
? options.presencePenalty
: (llmSettings.presence_penalty ?? unifiedDefaults.presence_penalty)
};
try {
if (window.KIMI_DEBUG_API_AUDIT) {
console.log(
"===== FULL SYSTEM PROMPT (OpenAI-Compatible) =====\n" +
systemPromptContent +
"\n===== END SYSTEM PROMPT ====="
);
}
const response = await fetch(baseUrl, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json"
},
body: JSON.stringify(payload)
});
if (!response.ok) {
let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
try {
const err = await response.json();
if (err?.error?.message) errorMessage = err.error.message;
} catch {}
throw new Error(errorMessage);
}
const data = await response.json();
const content = data?.choices?.[0]?.message?.content;
if (!content) throw new Error("Invalid API response - no content generated");
this.conversationContext.push(
{ role: "user", content: userMessage, timestamp: new Date().toISOString() },
{ role: "assistant", content: content, timestamp: new Date().toISOString() }
);
if (this.conversationContext.length > this.maxContextLength * 2) {
this.conversationContext = this.conversationContext.slice(-this.maxContextLength * 2);
}
// Approximate token usage and store temporarily for later persistence (single save point)
try {
const est = window.KimiTokenUtils?.estimate || (t => Math.ceil((t || "").length / 4));
const tokensIn = est(userMessage + " " + systemPromptContent);
const tokensOut = est(content);
window._lastKimiTokenUsage = { tokensIn, tokensOut };
if (!window.kimiMemory && this.db) {
// Update counters early so UI can reflect even if memory save occurs later
const character = await this.db.getSelectedCharacter();
const prevIn = Number(await this.db.getPreference(`totalTokensIn_${character}`, 0)) || 0;
const prevOut = Number(await this.db.getPreference(`totalTokensOut_${character}`, 0)) || 0;
await this.db.setPreference(`totalTokensIn_${character}`, prevIn + tokensIn);
await this.db.setPreference(`totalTokensOut_${character}`, prevOut + tokensOut);
}
} catch (tokenErr) {
console.warn("Token usage estimation failed:", tokenErr);
}
return content;
} catch (e) {
if (e.name === "TypeError" && e.message.includes("fetch")) {
throw new Error("Network connection error. Check your internet connection.");
}
throw e;
}
}
async chatWithOpenRouter(userMessage, options = {}) {
const apiKey = await this.db.getPreference("providerApiKey");
if (!apiKey) {
throw new Error("OpenRouter API key not configured");
}
const selectedLanguage = await this.db.getPreference("selectedLanguage", "en");
// languageInstruction removed (already integrated in personality prompt generation)
let languageInstruction = ""; // Kept for structural compatibility
const model = this.availableModels[this.currentModel];
const systemPromptContent = await this.assemblePrompt(userMessage);
const messages = [
{ role: "system", content: systemPromptContent },
...this.conversationContext.slice(-this.maxContextLength),
{ role: "user", content: userMessage }
];
// Normalize LLM options with safe defaults and DO NOT log sensitive payloads
const llmSettings = await this.db.getSetting("llm", {
temperature: 0.9,
maxTokens: 400,
top_p: 0.9,
frequency_penalty: 0.9,
presence_penalty: 0.8
});
const unifiedDefaults = { temperature: 0.9, maxTokens: 400, top_p: 0.9, frequency_penalty: 0.9, presence_penalty: 0.8 };
const payload = {
model: this.currentModel,
messages: messages,
temperature:
typeof options.temperature === "number"
? options.temperature
: (llmSettings.temperature ?? unifiedDefaults.temperature),
max_tokens:
typeof options.maxTokens === "number" ? options.maxTokens : (llmSettings.maxTokens ?? unifiedDefaults.maxTokens),
top_p: typeof options.topP === "number" ? options.topP : (llmSettings.top_p ?? unifiedDefaults.top_p),
frequency_penalty:
typeof options.frequencyPenalty === "number"
? options.frequencyPenalty
: (llmSettings.frequency_penalty ?? unifiedDefaults.frequency_penalty),
presence_penalty:
typeof options.presencePenalty === "number"
? options.presencePenalty
: (llmSettings.presence_penalty ?? unifiedDefaults.presence_penalty)
};
// ===== DEBUT AUDIT =====
if (window.KIMI_DEBUG_API_AUDIT) {
console.log("╔═══════════════════════════════════════════════════════════════════╗");
console.log("β•‘ πŸ” COMPLETE API AUDIT - SEND MESSAGE β•‘");
console.log("β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•");
console.log("πŸ“‹ 1. GENERAL INFORMATION:");
console.log(" πŸ“‘ URL API:", "https://openrouter.ai/api/v1/chat/completions");
console.log(" πŸ€– ModΓ¨le:", payload.model);
console.log(" 🎭 Personnage:", await this.db.getSelectedCharacter());
console.log(" πŸ—£οΈ Langue:", await this.db.getPreference("selectedLanguage", "en"));
console.log("\nπŸ“‹ 2. HEADERS HTTP:");
console.log(" πŸ”‘ Authorization: Bearer", apiKey.substring(0, 10) + "...");
console.log(" πŸ“„ Content-Type: application/json");
console.log(" 🌐 HTTP-Referer:", window.location.origin);
console.log(" 🏷️ X-Title: Kimi - Virtual Companion");
console.log("\nβš™οΈ 3. PARAMÈTRES LLM:");
console.log(" 🌑️ Temperature:", payload.temperature);
console.log(" πŸ“ Max Tokens:", payload.max_tokens);
console.log(" 🎯 Top P:", payload.top_p);
console.log(" πŸ”„ Frequency Penalty:", payload.frequency_penalty);
console.log(" πŸ‘€ Presence Penalty:", payload.presence_penalty);
console.log("\n🎭 4. PROMPT SYSTÈME GΓ‰NΓ‰RΓ‰:");
const systemMessage = payload.messages.find(m => m.role === "system");
if (systemMessage) {
console.log(" πŸ“ Longueur du prompt:", systemMessage.content.length, "caractΓ¨res");
console.log(" πŸ“„ CONTENU COMPLET DU PROMPT:");
console.log(" " + "─".repeat(80));
// Imprimer chaque ligne avec indentation
systemMessage.content.split(/\n/).forEach(l => console.log(" " + l));
console.log(" " + "─".repeat(80));
}
console.log("\nπŸ’¬ 5. CONTEXTE DE CONVERSATION:");
console.log(" πŸ“Š Nombre total de messages:", payload.messages.length);
console.log(" πŸ“‹ DΓ©tail des messages:");
payload.messages.forEach((msg, index) => {
if (msg.role === "system") {
console.log(` [${index}] 🎭 SYSTEM: ${msg.content.length} caractères`);
} else if (msg.role === "user") {
console.log(` [${index}] πŸ‘€ USER: "${msg.content}"`);
} else if (msg.role === "assistant") {
console.log(` [${index}] πŸ€– ASSISTANT: "${msg.content.substring(0, 120)}..."`);
}
});
const payloadSize = JSON.stringify(payload).length;
console.log("\nπŸ“¦ 6. TAILLE DU PAYLOAD:");
console.log(" πŸ“ Taille totale:", payloadSize, "caractΓ¨res");
console.log(" πŸ’Ύ Taille en KB:", Math.round((payloadSize / 1024) * 100) / 100, "KB");
console.log("\nπŸš€ Envoi en cours vers l'API...");
console.log("╔═══════════════════════════════════════════════════════════════════╗");
}
// ===== FIN AUDIT =====
if (window.DEBUG_SAFE_LOGS) {
console.debug("LLM payload meta:", {
model: payload.model,
temperature: payload.temperature,
max_tokens: payload.max_tokens
});
}
try {
// Basic retry with exponential backoff and jitter for 429/5xx
const maxAttempts = 3;
let attempt = 0;
let response;
while (attempt < maxAttempts) {
attempt++;
response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
"HTTP-Referer": window.location.origin,
"X-Title": "Kimi - Virtual Companion"
},
body: JSON.stringify(payload)
});
if (response.ok) break;
if (response.status === 429 || response.status >= 500) {
const base = 400;
const delay = base * Math.pow(2, attempt - 1) + Math.floor(Math.random() * 200);
await new Promise(r => setTimeout(r, delay));
continue;
}
break;
}
if (!response.ok) {
let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
let suggestions = [];
try {
const errorData = await response.json();
if (errorData.error) {
errorMessage = errorData.error.message || errorData.error.code || errorMessage;
// More explicit error messages with suggestions
if (response.status === 422) {
errorMessage = `Model \"${this.currentModel}\" not available on OpenRouter.`;
// Refresh available models from API and try best match once
try {
await this.refreshRemoteModels();
const best = this.findBestMatchingModelId(this.currentModel);
if (best && best !== this.currentModel) {
// Try once with corrected model
this.currentModel = best;
await this.db.setPreference("llmModelId", best);
this._notifyModelChanged();
const retryResponse = await fetch("https://openrouter.ai/api/v1/chat/completions", {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
"HTTP-Referer": window.location.origin,
"X-Title": "Kimi - Virtual Companion"
},
body: JSON.stringify({ ...payload, model: best })
});
if (retryResponse.ok) {
const retryData = await retryResponse.json();
const kimiResponse = retryData.choices?.[0]?.message?.content;
if (!kimiResponse) throw new Error("Invalid API response - no content generated");
this.conversationContext.push(
{ role: "user", content: userMessage, timestamp: new Date().toISOString() },
{ role: "assistant", content: kimiResponse, timestamp: new Date().toISOString() }
);
if (this.conversationContext.length > this.maxContextLength * 2) {
this.conversationContext = this.conversationContext.slice(-this.maxContextLength * 2);
}
return kimiResponse;
}
}
} catch (e) {
// Swallow refresh errors; will fall through to standard error handling
}
} else if (response.status === 401) {
errorMessage = "Invalid API key. Check your OpenRouter key in the settings.";
} else if (response.status === 429) {
errorMessage = "Rate limit reached. Please wait a moment before trying again.";
} else if (response.status === 402) {
errorMessage = "Insufficient credit on your OpenRouter account.";
}
}
} catch (parseError) {
console.warn("Unable to parse API error:", parseError);
}
console.error(`OpenRouter API error (${response.status}):`, errorMessage);
// Add suggestions to the error if available
const error = new Error(errorMessage);
if (suggestions.length > 0) {
error.suggestions = suggestions;
}
throw error;
}
const data = await response.json();
if (!data.choices || !data.choices[0] || !data.choices[0].message) {
throw new Error("Invalid API response - no content generated");
}
const kimiResponse = data.choices[0].message.content;
// Add to context
this.conversationContext.push(
{ role: "user", content: userMessage, timestamp: new Date().toISOString() },
{ role: "assistant", content: kimiResponse, timestamp: new Date().toISOString() }
);
// Limit context size
if (this.conversationContext.length > this.maxContextLength * 2) {
this.conversationContext = this.conversationContext.slice(-this.maxContextLength * 2);
}
// Token usage estimation (deferred save)
try {
const est = window.KimiTokenUtils?.estimate || (t => Math.ceil((t || "").length / 4));
const tokensIn = est(userMessage + " " + systemPromptContent);
const tokensOut = est(kimiResponse);
window._lastKimiTokenUsage = { tokensIn, tokensOut };
if (!window.kimiMemory && this.db) {
const character = await this.db.getSelectedCharacter();
const prevIn = Number(await this.db.getPreference(`totalTokensIn_${character}`, 0)) || 0;
const prevOut = Number(await this.db.getPreference(`totalTokensOut_${character}`, 0)) || 0;
await this.db.setPreference(`totalTokensIn_${character}`, prevIn + tokensIn);
await this.db.setPreference(`totalTokensOut_${character}`, prevOut + tokensOut);
}
} catch (e) {
console.warn("Token usage estimation failed (OpenRouter):", e);
}
return kimiResponse;
} catch (networkError) {
if (networkError.name === "TypeError" && networkError.message.includes("fetch")) {
throw new Error("Network connection error. Check your internet connection.");
}
throw networkError;
}
}
async chatWithLocal(userMessage, options = {}) {
try {
const selectedLanguage = await this.db.getPreference("selectedLanguage", "en");
let languageInstruction = ""; // Removed generic duplication
let systemPromptContent = await this.assemblePrompt(userMessage);
if (window.KIMI_DEBUG_API_AUDIT) {
console.log("===== FULL SYSTEM PROMPT (Local) =====\n" + systemPromptContent + "\n===== END SYSTEM PROMPT =====");
}
const response = await fetch("http://localhost:11434/api/chat", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
model: "gemma-3n-E4B-it-Q4_K_M.gguf",
messages: [
{ role: "system", content: systemPromptContent },
{ role: "user", content: userMessage }
],
stream: false
})
});
if (!response.ok) {
throw new Error("Ollama not available");
}
const data = await response.json();
const content = data?.message?.content || data?.choices?.[0]?.message?.content || "";
if (!content) throw new Error("Local model returned empty response");
// Add to context like other providers
this.conversationContext.push(
{ role: "user", content: userMessage, timestamp: new Date().toISOString() },
{ role: "assistant", content: content, timestamp: new Date().toISOString() }
);
if (this.conversationContext.length > this.maxContextLength * 2) {
this.conversationContext = this.conversationContext.slice(-this.maxContextLength * 2);
}
// Estimate token usage for local model (heuristic)
try {
const est = window.KimiTokenUtils?.estimate || (t => Math.ceil((t || "").length / 4));
const tokensIn = est(userMessage + " " + systemPromptContent);
const tokensOut = est(content);
window._lastKimiTokenUsage = { tokensIn, tokensOut };
const character = await this.db.getSelectedCharacter();
const prevIn = Number(await this.db.getPreference(`totalTokensIn_${character}`, 0)) || 0;
const prevOut = Number(await this.db.getPreference(`totalTokensOut_${character}`, 0)) || 0;
await this.db.setPreference(`totalTokensIn_${character}`, prevIn + tokensIn);
await this.db.setPreference(`totalTokensOut_${character}`, prevOut + tokensOut);
} catch (e) {
console.warn("Token usage estimation failed (local):", e);
}
return content;
} catch (error) {
console.warn("Local LLM not available:", error);
return this.getFallbackResponse(userMessage);
}
}
getFallbackResponse(userMessage, errorType = "api") {
// Use centralized fallback manager instead of duplicated logic
if (window.KimiFallbackManager) {
// Map error types to the correct format
const errorTypeMap = {
api: "api_error",
model: "model_error",
network: "network_error"
};
const mappedType = errorTypeMap[errorType] || "technical_error";
return window.KimiFallbackManager.getFallbackMessage(mappedType);
}
// Fallback to legacy system if KimiFallbackManager not available
const i18n = window.kimiI18nManager;
if (!i18n) {
return "Sorry, I'm having technical difficulties! πŸ’•";
}
return i18n.t("fallback_technical_error");
}
getFallbackKeywords(trait, type) {
const keywords = {
humor: {
positive: ["funny", "hilarious", "joke", "laugh", "amusing", "humorous", "smile", "witty", "playful"],
negative: ["boring", "sad", "serious", "cold", "dry", "depressing", "gloomy"]
},
intelligence: {
positive: [
"intelligent",
"smart",
"brilliant",
"logical",
"clever",
"wise",
"genius",
"thoughtful",
"insightful"
],
negative: ["stupid", "dumb", "foolish", "slow", "naive", "ignorant", "simple"]
},
romance: {
positive: ["cuddle", "love", "romantic", "kiss", "tenderness", "passion", "charming", "adorable", "sweet"],
negative: ["cold", "distant", "indifferent", "rejection", "loneliness", "breakup", "sad"]
},
affection: {
positive: ["affection", "tenderness", "close", "warmth", "kind", "caring", "cuddle", "love", "adore"],
negative: ["mean", "cold", "indifferent", "distant", "rejection", "hate", "hostile"]
},
playfulness: {
positive: ["play", "game", "tease", "mischievous", "fun", "amusing", "playful", "joke", "frolic"],
negative: ["serious", "boring", "strict", "rigid", "monotonous", "tedious"]
},
empathy: {
positive: ["listen", "understand", "empathy", "support", "help", "comfort", "compassion", "caring", "kindness"],
negative: ["indifferent", "cold", "selfish", "ignore", "despise", "hostile", "uncaring"]
}
};
return keywords[trait]?.[type] || [];
}
// MΓ©moire temporaire pour l'accumulation nΓ©gative par trait
_negativeStreaks = {};
async updatePersonalityFromResponse(userMessage, kimiResponse) {
// Use unified emotion system for personality updates
if (window.kimiEmotionSystem) {
return await window.kimiEmotionSystem.updatePersonalityFromConversation(
userMessage,
kimiResponse,
await this.db.getSelectedCharacter()
);
}
// Legacy fallback (should not be reached)
console.warn("Unified emotion system not available, skipping personality update");
}
async getModelStats() {
const models = await this.db.getAllLLMModels();
const currentModelInfo = this.availableModels[this.currentModel];
return {
current: {
id: this.currentModel,
info: currentModelInfo
},
available: this.availableModels,
configured: models,
contextLength: this.conversationContext.length
};
}
async testModel(modelId, testMessage = "Test API ok?") {
// Ancienne mΓ©thode de test (non minimaliste)
return await this.testApiKeyMinimal(modelId);
}
/**
* Test API minimaliste et centralisΓ© pour tous les providers compatibles.
* Envoie uniquement un prompt système court et un message utilisateur dans la langue choisie.
* Aucun contexte, aucune mémoire, aucun paramètre superflu.
* @param {string} modelId - ID du modèle à tester
* @returns {Promise<{success: boolean, response?: string, error?: string}>}
*/
async testApiKeyMinimal(modelId) {
const originalModel = this.currentModel;
try {
await this.setCurrentModel(modelId);
const provider = await this.db.getPreference("llmProvider", "openrouter");
const lang = await this.db.getPreference("selectedLanguage", "en");
let testWord;
switch (lang) {
case "fr":
testWord = "Bonjour";
break;
case "es":
testWord = "Hola";
break;
case "de":
testWord = "Hallo";
break;
case "it":
testWord = "Ciao";
break;
case "ja":
testWord = "こんにけは";
break;
case "zh":
testWord = "δ½ ε₯½";
break;
default:
testWord = "Hello";
}
const systemPrompt = "You are a helpful assistant.";
let apiKey = await this.db.getPreference("providerApiKey");
let baseUrl = "";
let payload = {
model: modelId,
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: testWord }
],
max_tokens: 2
};
let headers = { "Content-Type": "application/json" };
if (provider === "openrouter") {
baseUrl = "https://openrouter.ai/api/v1/chat/completions";
headers["Authorization"] = `Bearer ${apiKey}`;
headers["HTTP-Referer"] = window.location.origin;
headers["X-Title"] = "Kimi - Virtual Companion";
} else if (["openai", "groq", "together", "deepseek", "openai-compatible"].includes(provider)) {
baseUrl = await this.db.getPreference("llmBaseUrl", "https://api.openai.com/v1/chat/completions");
headers["Authorization"] = `Bearer ${apiKey}`;
} else if (provider === "ollama") {
baseUrl = "http://localhost:11434/api/chat";
payload = {
model: modelId,
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: testWord }
],
stream: false
};
} else {
throw new Error("Unknown provider: " + provider);
}
const response = await fetch(baseUrl, {
method: "POST",
headers,
body: JSON.stringify(payload)
});
if (!response.ok) {
const error = await response.text();
return { success: false, error };
}
const data = await response.json();
let content = "";
if (provider === "ollama") {
content = data?.message?.content || data?.choices?.[0]?.message?.content || "";
} else {
content = data?.choices?.[0]?.message?.content || "";
}
return { success: true, response: content };
} catch (error) {
return { success: false, error: error.message };
} finally {
await this.setCurrentModel(originalModel);
}
}
// Complete model diagnosis
async diagnoseModel(modelId) {
const model = this.availableModels[modelId];
if (!model) {
return {
available: false,
error: "Model not found in local list"
};
}
// Check availability on OpenRouter
try {
// getAvailableModelsFromAPI removed
return {
available: true,
model: model,
pricing: model.pricing
};
} catch (error) {
return {
available: false,
error: `Unable to check: ${error.message}`
};
}
}
// Fetch models from OpenRouter API and merge into availableModels
async refreshRemoteModels() {
if (this._isRefreshingModels) return;
this._isRefreshingModels = true;
try {
const apiKey = await this.db.getPreference("providerApiKey", "");
const res = await fetch("https://openrouter.ai/api/v1/models", {
method: "GET",
headers: {
"Content-Type": "application/json",
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
"HTTP-Referer": window.location.origin,
"X-Title": "Kimi - Virtual Companion"
}
});
if (!res.ok) {
throw new Error(`Unable to fetch models: HTTP ${res.status}`);
}
const data = await res.json();
if (!data?.data || !Array.isArray(data.data)) {
throw new Error("Invalid models response format");
}
// Build a fresh map while preserving local/ollama entry
const newMap = {};
data.data.forEach(m => {
if (!m?.id) return;
const id = m.id;
const provider = m?.id?.split("/")?.[0] || "OpenRouter";
let pricing;
const p = m?.pricing;
if (p) {
const unitRaw = ((p.unit || p.per || p.units || "") + "").toLowerCase();
let unitTokens = 1;
if (unitRaw) {
if (unitRaw.includes("1m")) unitTokens = 1000000;
else if (unitRaw.includes("1k") || unitRaw.includes("thousand")) unitTokens = 1000;
else {
const num = parseFloat(unitRaw.replace(/[^0-9.]/g, ""));
if (Number.isFinite(num) && num > 0) {
if (unitRaw.includes("m")) unitTokens = num * 1000000;
else if (unitRaw.includes("k")) unitTokens = num * 1000;
else unitTokens = num;
} else if (unitRaw.includes("token")) {
unitTokens = 1;
}
}
}
const toPerMillion = v => {
const n = typeof v === "number" ? v : parseFloat(v);
if (!Number.isFinite(n)) return undefined;
return n * (1000000 / unitTokens);
};
if (typeof p.input !== "undefined" || typeof p.output !== "undefined") {
pricing = {
input: toPerMillion(p.input),
output: toPerMillion(p.output)
};
} else if (typeof p.prompt !== "undefined" || typeof p.completion !== "undefined") {
pricing = {
input: toPerMillion(p.prompt),
output: toPerMillion(p.completion)
};
} else {
pricing = { input: undefined, output: undefined };
}
} else {
pricing = { input: undefined, output: undefined };
}
newMap[id] = {
name: m.name || id,
provider,
type: "openrouter",
contextWindow: m.context_length || m?.context_window || 128000,
pricing,
strengths: (m?.tags || []).slice(0, 4)
};
});
// Keep local model entry
if (this.availableModels["local/ollama"]) {
newMap["local/ollama"] = this.availableModels["local/ollama"];
}
this.recommendedModelIds.forEach(id => {
const curated = this.defaultModels[id];
if (curated) {
newMap[id] = { ...(newMap[id] || {}), ...curated };
}
});
this.availableModels = newMap;
this._remoteModelsLoaded = true;
} finally {
this._isRefreshingModels = false;
}
}
// Try to find best matching model id from remote list when an ID is stale
findBestMatchingModelId(preferredId) {
if (this.availableModels[preferredId]) return preferredId;
const id = (preferredId || "").toLowerCase();
const tokens = id.split(/[\/:\-_.]+/).filter(Boolean);
let best = null;
let bestScore = -1;
Object.keys(this.availableModels).forEach(candidateId => {
const c = candidateId.toLowerCase();
let score = 0;
tokens.forEach(t => {
if (!t) return;
if (c.includes(t)) score += 1;
});
// Give extra weight to common markers
if (c.includes("instruct")) score += 0.5;
if (c.includes("mistral") && id.includes("mistral")) score += 0.5;
if (c.includes("small") && id.includes("small")) score += 0.5;
if (score > bestScore) {
bestScore = score;
best = candidateId;
}
});
// Avoid returning unrelated local model unless nothing else
if (best === "local/ollama" && Object.keys(this.availableModels).length > 1) {
return null;
}
return best;
}
_notifyModelChanged() {
try {
const detail = { id: this.currentModel };
if (typeof window !== "undefined" && typeof window.dispatchEvent === "function") {
window.dispatchEvent(new CustomEvent("llmModelChanged", { detail }));
}
} catch (e) {}
}
}
// Export for usage
window.KimiLLMManager = KimiLLMManager;
export default KimiLLMManager;