File size: 8,465 Bytes
d58d97b cbce8db d58d97b cbce8db d58d97b |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 |
/*
ELYSIA CODE COMPANION v1.2.2 - API Layer
OpenRouter integration for Elysia AI
*/
import Utils from "./utils.js";
const API = {
baseURL: "https://openrouter.ai/api/v1/chat/completions",
// Call Elysia via OpenRouter
async call(messages, options = {}) {
const apiKey = options.apiKey || Utils.storage.get("apiKey");
const model = options.model || Utils.storage.get("model", "x-ai/grok-4.1-fast");
// Validate API key
const validation = Utils.validateApiKey(apiKey);
if (!validation.valid) {
throw new Error(validation.error);
}
// Create abort controller for timeout
const controller = new AbortController();
let timeoutId = null;
const timeoutMs = options.timeout || 30000; // 30s default
timeoutId = setTimeout(() => {
controller.abort();
}, timeoutMs);
try {
const response = await fetch(this.baseURL, {
method: "POST",
signal: controller.signal,
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
"HTTP-Referer": window.location.href,
"X-Title": "Elysia Code Companion"
},
body: JSON.stringify({
model,
messages,
temperature: options.temperature || 0.7,
max_tokens: options.maxTokens || Utils.storage.get("maxResponseTokens", 4000),
stream: options.stream || false
})
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error?.message || `API error: ${response.status}`);
}
if (options.stream) {
return response; // Return response for streaming
}
const data = await response.json();
if (timeoutId !== null) {
clearTimeout(timeoutId);
timeoutId = null;
}
return {
content: data.choices[0].message.content,
model: data.model,
usage: data.usage
};
} catch (err) {
if (timeoutId !== null) {
clearTimeout(timeoutId);
timeoutId = null;
}
console.error("API call failed:", err);
// Provide helpful error messages
if (err.message.includes("Failed to fetch")) {
throw new Error("Network error. Check your internet connection.");
} else if (err.message.includes("401") || err.message.includes("403")) {
throw new Error("Invalid API key. Please check your settings.");
} else if (err.message.includes("429")) {
throw new Error("Rate limit exceeded. Please wait and try again.");
}
throw err;
}
},
// Stream Chat (for real-time responses)
async stream(messages, onChunk, options = {}) {
const apiKey = options.apiKey || Utils.storage.get("apiKey");
const model = options.model || Utils.storage.get("model", "x-ai/grok-4.1-fast");
const validation = Utils.validateApiKey(apiKey);
if (!validation.valid) {
throw new Error(validation.error);
}
// Create abort controller for timeout and cancellation
const controller = options.signal ? null : new AbortController();
const signal = options.signal || controller?.signal;
const timeoutMs = options.timeout || 120000; // 2 min default for streaming
let timeoutId = null;
if (controller) {
timeoutId = setTimeout(() => {
controller.abort();
}, timeoutMs);
}
try {
const response = await fetch(this.baseURL, {
method: "POST",
signal,
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
"HTTP-Referer": window.location.href,
"X-Title": "Elysia Code Companion"
},
body: JSON.stringify({
model,
messages,
temperature: options.temperature || 0.7,
max_tokens: options.maxTokens || Utils.storage.get("maxResponseTokens", 4000),
stream: true
})
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error?.message || `API error: ${response.status}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let fullContent = "";
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 content = parsed.choices[0]?.delta?.content || "";
if (content) {
fullContent += content;
onChunk(content, fullContent);
}
} catch (err) {
console.warn("Failed to parse chunk:", err);
}
}
}
}
if (timeoutId) clearTimeout(timeoutId);
return fullContent;
} catch (err) {
if (timeoutId) clearTimeout(timeoutId);
console.error("Streaming failed:", err);
// Handle abort/cancellation
if (err.name === "AbortError") {
throw new Error("Request cancelled or timed out.");
}
// Provide user-friendly error messages
if (err.message.includes("API key")) {
throw new Error("Invalid API key. Please check your settings.");
} else if (err.message.includes("rate limit")) {
throw new Error("Rate limit exceeded. Please wait a moment and try again.");
} else if (err.message.includes("network")) {
throw new Error("Network error. Please check your internet connection.");
}
throw new Error(`API Error: ${err.message}`);
}
},
// Build System Prompt for Code Companion
getSystemPrompt(context = {}) {
const { folderName, fileCount, files } = context;
let prompt = `You are Code Companion, an AI assistant specialized in code analysis and development help.
**Your Role:**
- Analyze code structure, find bugs, and suggest improvements
- Explain complex code in simple, clear terms
- Provide actionable insights with examples
- Be concise, helpful, and professional
**Current Context:**`;
if (folderName) {
prompt += `\n- Project: ${folderName}`;
prompt += `\n- Files available: ${fileCount || 0}`;
} else {
prompt += `\n- No folder opened yet`;
}
if (files && files.length > 0) {
prompt += `\n\n**Files in context:**\n`;
files.forEach(file => {
prompt += `\n### ${file.name}\n\`\`\`${file.language || ""}\n${file.content}\n\`\`\`\n`;
});
}
prompt += `\n\n**Response Guidelines:**
- Be concise and direct
- Use code examples when helpful
- Highlight critical issues with π¨
- Suggest improvements with π‘
- Mark good practices with β
- Focus on practical, actionable advice`;
return prompt;
}
};
export default API;
|