aether-ai-agent-cli 1.1.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aether_ai_agent_cli-1.1.4.dist-info/METADATA +309 -0
- aether_ai_agent_cli-1.1.4.dist-info/RECORD +25 -0
- aether_ai_agent_cli-1.1.4.dist-info/WHEEL +5 -0
- aether_ai_agent_cli-1.1.4.dist-info/entry_points.txt +2 -0
- aether_ai_agent_cli-1.1.4.dist-info/licenses/LICENSE +21 -0
- aether_ai_agent_cli-1.1.4.dist-info/top_level.txt +1 -0
- aether_pip/__init__.py +1 -0
- aether_pip/cli.py +49 -0
- aether_pip/node_project/bin/aether.js +10 -0
- aether_pip/node_project/package-lock.json +794 -0
- aether_pip/node_project/package.json +46 -0
- aether_pip/node_project/src/ai/fallback.js +179 -0
- aether_pip/node_project/src/ai/google.js +87 -0
- aether_pip/node_project/src/ai/providers.js +203 -0
- aether_pip/node_project/src/ai/router.js +114 -0
- aether_pip/node_project/src/ai/universal.js +507 -0
- aether_pip/node_project/src/ai/xai.js +50 -0
- aether_pip/node_project/src/chat.js +1018 -0
- aether_pip/node_project/src/cli.js +679 -0
- aether_pip/node_project/src/config.js +214 -0
- aether_pip/node_project/src/file-parser.js +94 -0
- aether_pip/node_project/src/modes.js +121 -0
- aether_pip/node_project/src/ui/banner.js +60 -0
- aether_pip/node_project/src/ui/spinner.js +43 -0
- aether_pip/node_project/src/ui/theme.js +169 -0
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════
|
|
2
|
+
// AETHER AI CLI — Universal API Caller
|
|
3
|
+
// Handles OpenAI-compatible, Google, Anthropic, and Cohere APIs
|
|
4
|
+
// ═══════════════════════════════════════════════════════════
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Calls any OpenAI-compatible API (OpenAI, Groq, Together, Mistral, xAI, etc.)
|
|
8
|
+
* @param {string} prompt - User message
|
|
9
|
+
* @param {string} systemPrompt - System prompt
|
|
10
|
+
* @param {string} apiKey - API key
|
|
11
|
+
* @param {string} baseUrl - Full endpoint URL
|
|
12
|
+
* @param {string} model - Model identifier
|
|
13
|
+
* @param {string} providerName - For error messages
|
|
14
|
+
* @returns {Promise<{ text: string, provider: string, model: string }>}
|
|
15
|
+
*/
|
|
16
|
+
export async function callOpenAICompatible(prompt, systemPrompt, apiKey, baseUrl, model, providerName, onToken, history = []) {
|
|
17
|
+
const isStreaming = typeof onToken === "function";
|
|
18
|
+
const formattedHistory = history.map(h => ({
|
|
19
|
+
role: h.role === "assistant" ? "assistant" : "user",
|
|
20
|
+
content: h.content
|
|
21
|
+
}));
|
|
22
|
+
const body = {
|
|
23
|
+
model,
|
|
24
|
+
messages: [
|
|
25
|
+
{ role: "system", content: systemPrompt },
|
|
26
|
+
...formattedHistory,
|
|
27
|
+
{ role: "user", content: prompt },
|
|
28
|
+
],
|
|
29
|
+
temperature: 0.7,
|
|
30
|
+
max_tokens: 4096,
|
|
31
|
+
...(isStreaming ? { stream: true } : {}),
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const headers = {
|
|
35
|
+
"Content-Type": "application/json",
|
|
36
|
+
Authorization: `Bearer ${apiKey}`,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// OpenRouter requires extra headers
|
|
40
|
+
if (baseUrl.includes("openrouter.ai")) {
|
|
41
|
+
headers["HTTP-Referer"] = "https://github.com/Krylo-60/aether-ai-cli";
|
|
42
|
+
headers["X-Title"] = "Aether AI CLI";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const response = await fetch(baseUrl, {
|
|
46
|
+
method: "POST",
|
|
47
|
+
headers,
|
|
48
|
+
body: JSON.stringify(body),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (!response.ok) {
|
|
52
|
+
const errorBody = await response.text().catch(() => "");
|
|
53
|
+
throw new Error(`${providerName} API error (${response.status}): ${response.statusText}. ${errorBody}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (isStreaming && response.body && typeof response.body.getReader === "function") {
|
|
57
|
+
const reader = response.body.getReader();
|
|
58
|
+
const decoder = new TextDecoder();
|
|
59
|
+
let buffer = "";
|
|
60
|
+
let fullText = "";
|
|
61
|
+
|
|
62
|
+
while (true) {
|
|
63
|
+
const { done, value } = await reader.read();
|
|
64
|
+
if (done) break;
|
|
65
|
+
buffer += decoder.decode(value, { stream: true });
|
|
66
|
+
|
|
67
|
+
const lines = buffer.split("\n");
|
|
68
|
+
buffer = lines.pop(); // Keep the partial line
|
|
69
|
+
|
|
70
|
+
for (const line of lines) {
|
|
71
|
+
const trimmed = line.trim();
|
|
72
|
+
if (!trimmed) continue;
|
|
73
|
+
if (trimmed === "data: [DONE]") continue;
|
|
74
|
+
if (trimmed.startsWith("data: ")) {
|
|
75
|
+
try {
|
|
76
|
+
const data = JSON.parse(trimmed.slice(6));
|
|
77
|
+
const content = data?.choices?.[0]?.delta?.content || "";
|
|
78
|
+
if (content) {
|
|
79
|
+
onToken(content);
|
|
80
|
+
fullText += content;
|
|
81
|
+
}
|
|
82
|
+
} catch (e) {
|
|
83
|
+
// Ignore partial line errors
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Flush remaining buffer
|
|
89
|
+
if (buffer && buffer.startsWith("data: ")) {
|
|
90
|
+
try {
|
|
91
|
+
const data = JSON.parse(buffer.slice(6));
|
|
92
|
+
const content = data?.choices?.[0]?.delta?.content || "";
|
|
93
|
+
if (content) {
|
|
94
|
+
onToken(content);
|
|
95
|
+
fullText += content;
|
|
96
|
+
}
|
|
97
|
+
} catch (e) {}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!fullText) {
|
|
101
|
+
throw new Error(`${providerName} returned empty response`);
|
|
102
|
+
}
|
|
103
|
+
return { text: fullText, provider: providerName.toLowerCase(), model };
|
|
104
|
+
} else {
|
|
105
|
+
const data = await response.json();
|
|
106
|
+
const text = data?.choices?.[0]?.message?.content;
|
|
107
|
+
|
|
108
|
+
if (!text) {
|
|
109
|
+
throw new Error(`${providerName} returned empty response`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return { text, provider: providerName.toLowerCase(), model };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Calls the Google Gemini API (non-OpenAI format).
|
|
118
|
+
* @param {string} prompt - User message
|
|
119
|
+
* @param {string} systemPrompt - System prompt
|
|
120
|
+
* @param {string} apiKey - Google API key
|
|
121
|
+
* @param {string} model - Model name
|
|
122
|
+
* @returns {Promise<{ text: string, provider: string, model: string }>}
|
|
123
|
+
*/
|
|
124
|
+
export async function callGoogleGemini(prompt, systemPrompt, apiKey, model = "gemini-2.5-flash", onToken, history = []) {
|
|
125
|
+
const BASE = "https://generativelanguage.googleapis.com/v1beta/models";
|
|
126
|
+
const isStreaming = typeof onToken === "function";
|
|
127
|
+
let currentHistory = [...history];
|
|
128
|
+
|
|
129
|
+
if (isStreaming) {
|
|
130
|
+
let fullText = "";
|
|
131
|
+
let currentPrompt = prompt;
|
|
132
|
+
let continuations = 0;
|
|
133
|
+
const MAX = 3;
|
|
134
|
+
|
|
135
|
+
while (continuations <= MAX) {
|
|
136
|
+
const url = `${BASE}/${model}:streamGenerateContent?key=${apiKey}`;
|
|
137
|
+
const formattedHistory = currentHistory.map(h => ({
|
|
138
|
+
role: h.role === "assistant" ? "model" : "user",
|
|
139
|
+
parts: [{ text: h.content }]
|
|
140
|
+
}));
|
|
141
|
+
const body = {
|
|
142
|
+
systemInstruction: { parts: [{ text: systemPrompt }] },
|
|
143
|
+
contents: [
|
|
144
|
+
...formattedHistory,
|
|
145
|
+
{ role: "user", parts: [{ text: currentPrompt }] }
|
|
146
|
+
],
|
|
147
|
+
generationConfig: { temperature: 0.7, maxOutputTokens: 8192 },
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const response = await fetch(url, {
|
|
151
|
+
method: "POST",
|
|
152
|
+
headers: { "Content-Type": "application/json" },
|
|
153
|
+
body: JSON.stringify(body),
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
if (!response.ok) {
|
|
157
|
+
const errorBody = await response.text().catch(() => "");
|
|
158
|
+
throw new Error(`Gemini API error (${response.status}): ${response.statusText}. ${errorBody}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
let streamedTextInThisTurn = "";
|
|
162
|
+
|
|
163
|
+
if (!response.body || typeof response.body.getReader !== "function") {
|
|
164
|
+
// Fallback to non-streaming if response body is not streamable (e.g. in unit tests)
|
|
165
|
+
const data = await response.json();
|
|
166
|
+
let chunkText = "";
|
|
167
|
+
let finishReason = "";
|
|
168
|
+
if (Array.isArray(data)) {
|
|
169
|
+
for (const chunk of data) {
|
|
170
|
+
const partText = chunk.candidates?.[0]?.content?.parts?.map((p) => p.text).filter(Boolean).join("") || "";
|
|
171
|
+
chunkText += partText;
|
|
172
|
+
finishReason = chunk.candidates?.[0]?.finishReason || finishReason;
|
|
173
|
+
}
|
|
174
|
+
} else {
|
|
175
|
+
chunkText = data.candidates?.[0]?.content?.parts?.map((p) => p.text).filter(Boolean).join("") || "";
|
|
176
|
+
finishReason = data.candidates?.[0]?.finishReason || finishReason;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (chunkText) {
|
|
180
|
+
onToken(chunkText);
|
|
181
|
+
fullText += chunkText;
|
|
182
|
+
streamedTextInThisTurn += chunkText;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (finishReason === "MAX_TOKENS" && continuations < MAX) {
|
|
186
|
+
currentHistory.push({ role: "user", content: currentPrompt });
|
|
187
|
+
currentHistory.push({ role: "assistant", content: streamedTextInThisTurn });
|
|
188
|
+
continuations++;
|
|
189
|
+
currentPrompt = "Continue your previous response from exactly where you left off.";
|
|
190
|
+
} else {
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
const reader = response.body.getReader();
|
|
195
|
+
const decoder = new TextDecoder();
|
|
196
|
+
let buffer = "";
|
|
197
|
+
let finishReason = "";
|
|
198
|
+
|
|
199
|
+
while (true) {
|
|
200
|
+
const { done, value } = await reader.read();
|
|
201
|
+
if (done) break;
|
|
202
|
+
buffer += decoder.decode(value, { stream: true });
|
|
203
|
+
|
|
204
|
+
let braceCount = 0;
|
|
205
|
+
let jsonStart = -1;
|
|
206
|
+
let inString = false;
|
|
207
|
+
let escape = false;
|
|
208
|
+
|
|
209
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
210
|
+
const char = buffer[i];
|
|
211
|
+
if (inString) {
|
|
212
|
+
if (escape) {
|
|
213
|
+
escape = false;
|
|
214
|
+
} else if (char === "\\") {
|
|
215
|
+
escape = true;
|
|
216
|
+
} else if (char === '"') {
|
|
217
|
+
inString = false;
|
|
218
|
+
}
|
|
219
|
+
} else {
|
|
220
|
+
if (char === '"') {
|
|
221
|
+
inString = true;
|
|
222
|
+
} else if (char === "{") {
|
|
223
|
+
if (braceCount === 0) {
|
|
224
|
+
jsonStart = i;
|
|
225
|
+
}
|
|
226
|
+
braceCount++;
|
|
227
|
+
} else if (char === "}") {
|
|
228
|
+
braceCount--;
|
|
229
|
+
if (braceCount === 0 && jsonStart !== -1) {
|
|
230
|
+
const jsonStr = buffer.slice(jsonStart, i + 1);
|
|
231
|
+
try {
|
|
232
|
+
const obj = JSON.parse(jsonStr);
|
|
233
|
+
const text = obj.candidates?.[0]?.content?.parts?.map((p) => p.text).filter(Boolean).join("") || "";
|
|
234
|
+
finishReason = obj.candidates?.[0]?.finishReason || finishReason;
|
|
235
|
+
if (text) {
|
|
236
|
+
onToken(text);
|
|
237
|
+
fullText += text;
|
|
238
|
+
streamedTextInThisTurn += text;
|
|
239
|
+
}
|
|
240
|
+
} catch (e) {
|
|
241
|
+
// Ignore parse errors
|
|
242
|
+
}
|
|
243
|
+
buffer = buffer.slice(i + 1);
|
|
244
|
+
i = -1;
|
|
245
|
+
jsonStart = -1;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (finishReason === "MAX_TOKENS" && continuations < MAX) {
|
|
253
|
+
currentHistory.push({ role: "user", content: currentPrompt });
|
|
254
|
+
currentHistory.push({ role: "assistant", content: streamedTextInThisTurn });
|
|
255
|
+
continuations++;
|
|
256
|
+
currentPrompt = "Continue your previous response from exactly where you left off.";
|
|
257
|
+
} else {
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (!fullText.trim()) throw new Error("Gemini returned empty response");
|
|
264
|
+
return { text: fullText, provider: "google", model };
|
|
265
|
+
} else {
|
|
266
|
+
let fullText = "";
|
|
267
|
+
let currentPrompt = prompt;
|
|
268
|
+
let continuations = 0;
|
|
269
|
+
const MAX = 3;
|
|
270
|
+
|
|
271
|
+
while (continuations <= MAX) {
|
|
272
|
+
const url = `${BASE}/${model}:generateContent?key=${apiKey}`;
|
|
273
|
+
const formattedHistory = currentHistory.map(h => ({
|
|
274
|
+
role: h.role === "assistant" ? "model" : "user",
|
|
275
|
+
parts: [{ text: h.content }]
|
|
276
|
+
}));
|
|
277
|
+
const body = {
|
|
278
|
+
systemInstruction: { parts: [{ text: systemPrompt }] },
|
|
279
|
+
contents: [
|
|
280
|
+
...formattedHistory,
|
|
281
|
+
{ role: "user", parts: [{ text: currentPrompt }] }
|
|
282
|
+
],
|
|
283
|
+
generationConfig: { temperature: 0.7, maxOutputTokens: 8192 },
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const response = await fetch(url, {
|
|
287
|
+
method: "POST",
|
|
288
|
+
headers: { "Content-Type": "application/json" },
|
|
289
|
+
body: JSON.stringify(body),
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
if (!response.ok) {
|
|
293
|
+
const errorBody = await response.text().catch(() => "");
|
|
294
|
+
throw new Error(`Gemini API error (${response.status}): ${response.statusText}. ${errorBody}`);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const data = await response.json();
|
|
298
|
+
if (data.promptFeedback?.blockReason) {
|
|
299
|
+
throw new Error(`Content blocked: ${data.promptFeedback.blockReason}`);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const candidate = data.candidates?.[0];
|
|
303
|
+
if (!candidate) throw new Error("Gemini returned no candidates");
|
|
304
|
+
|
|
305
|
+
const chunkText = candidate.content?.parts?.map((p) => p.text).filter(Boolean).join("") || "";
|
|
306
|
+
fullText += chunkText;
|
|
307
|
+
|
|
308
|
+
if (candidate.finishReason === "MAX_TOKENS" && continuations < MAX) {
|
|
309
|
+
currentHistory.push({ role: "user", content: currentPrompt });
|
|
310
|
+
currentHistory.push({ role: "assistant", content: chunkText });
|
|
311
|
+
continuations++;
|
|
312
|
+
currentPrompt = "Continue your previous response from exactly where you left off.";
|
|
313
|
+
} else {
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (!fullText.trim()) throw new Error("Gemini returned empty response");
|
|
319
|
+
return { text: fullText, provider: "google", model };
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Calls the Anthropic Claude API.
|
|
325
|
+
* @param {string} prompt - User message
|
|
326
|
+
* @param {string} systemPrompt - System prompt
|
|
327
|
+
* @param {string} apiKey - Anthropic API key
|
|
328
|
+
* @param {string} model - Model name
|
|
329
|
+
* @returns {Promise<{ text: string, provider: string, model: string }>}
|
|
330
|
+
*/
|
|
331
|
+
export async function callAnthropic(prompt, systemPrompt, apiKey, model = "claude-sonnet-4-20250514", onToken, history = []) {
|
|
332
|
+
const url = "https://api.anthropic.com/v1/messages";
|
|
333
|
+
const isStreaming = typeof onToken === "function";
|
|
334
|
+
const formattedHistory = history.map(h => ({
|
|
335
|
+
role: h.role === "assistant" ? "assistant" : "user",
|
|
336
|
+
content: h.content
|
|
337
|
+
}));
|
|
338
|
+
const body = {
|
|
339
|
+
model,
|
|
340
|
+
max_tokens: 4096,
|
|
341
|
+
system: systemPrompt,
|
|
342
|
+
messages: [
|
|
343
|
+
...formattedHistory,
|
|
344
|
+
{ role: "user", content: prompt }
|
|
345
|
+
],
|
|
346
|
+
...(isStreaming ? { stream: true } : {}),
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
const response = await fetch(url, {
|
|
350
|
+
method: "POST",
|
|
351
|
+
headers: {
|
|
352
|
+
"Content-Type": "application/json",
|
|
353
|
+
"x-api-key": apiKey,
|
|
354
|
+
"anthropic-version": "2023-06-01",
|
|
355
|
+
},
|
|
356
|
+
body: JSON.stringify(body),
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
if (!response.ok) {
|
|
360
|
+
const errorBody = await response.text().catch(() => "");
|
|
361
|
+
throw new Error(`Anthropic API error (${response.status}): ${response.statusText}. ${errorBody}`);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (isStreaming && response.body && typeof response.body.getReader === "function") {
|
|
365
|
+
const reader = response.body.getReader();
|
|
366
|
+
const decoder = new TextDecoder();
|
|
367
|
+
let buffer = "";
|
|
368
|
+
let fullText = "";
|
|
369
|
+
|
|
370
|
+
while (true) {
|
|
371
|
+
const { done, value } = await reader.read();
|
|
372
|
+
if (done) break;
|
|
373
|
+
buffer += decoder.decode(value, { stream: true });
|
|
374
|
+
|
|
375
|
+
const lines = buffer.split("\n");
|
|
376
|
+
buffer = lines.pop();
|
|
377
|
+
|
|
378
|
+
for (const line of lines) {
|
|
379
|
+
const trimmed = line.trim();
|
|
380
|
+
if (!trimmed) continue;
|
|
381
|
+
if (trimmed.startsWith("data: ")) {
|
|
382
|
+
try {
|
|
383
|
+
const data = JSON.parse(trimmed.slice(6));
|
|
384
|
+
if (data.type === "content_block_delta" && data.delta?.text) {
|
|
385
|
+
onToken(data.delta.text);
|
|
386
|
+
fullText += data.delta.text;
|
|
387
|
+
}
|
|
388
|
+
} catch (e) {
|
|
389
|
+
// Ignore partial JSON
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
// Flush remaining
|
|
395
|
+
if (buffer && buffer.startsWith("data: ")) {
|
|
396
|
+
try {
|
|
397
|
+
const data = JSON.parse(buffer.slice(6));
|
|
398
|
+
if (data.type === "content_block_delta" && data.delta?.text) {
|
|
399
|
+
onToken(data.delta.text);
|
|
400
|
+
fullText += data.delta.text;
|
|
401
|
+
}
|
|
402
|
+
} catch (e) {}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (!fullText) throw new Error("Anthropic returned empty response");
|
|
406
|
+
return { text: fullText, provider: "anthropic", model };
|
|
407
|
+
} else {
|
|
408
|
+
const data = await response.json();
|
|
409
|
+
const text = data?.content?.[0]?.text;
|
|
410
|
+
|
|
411
|
+
if (!text) throw new Error("Anthropic returned empty response");
|
|
412
|
+
return { text, provider: "anthropic", model };
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Calls the Cohere API (v2 chat format).
|
|
418
|
+
* @param {string} prompt - User message
|
|
419
|
+
* @param {string} systemPrompt - System prompt
|
|
420
|
+
* @param {string} apiKey - Cohere API key
|
|
421
|
+
* @param {string} model - Model name
|
|
422
|
+
* @returns {Promise<{ text: string, provider: string, model: string }>}
|
|
423
|
+
*/
|
|
424
|
+
export async function callCohere(prompt, systemPrompt, apiKey, model = "command-r-plus", onToken, history = []) {
|
|
425
|
+
const url = "https://api.cohere.com/v2/chat";
|
|
426
|
+
const isStreaming = typeof onToken === "function";
|
|
427
|
+
const formattedHistory = history.map(h => ({
|
|
428
|
+
role: h.role === "assistant" ? "assistant" : "user",
|
|
429
|
+
content: h.content
|
|
430
|
+
}));
|
|
431
|
+
const body = {
|
|
432
|
+
model,
|
|
433
|
+
messages: [
|
|
434
|
+
{ role: "system", content: systemPrompt },
|
|
435
|
+
...formattedHistory,
|
|
436
|
+
{ role: "user", content: prompt },
|
|
437
|
+
],
|
|
438
|
+
...(isStreaming ? { stream: true } : {}),
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
const response = await fetch(url, {
|
|
442
|
+
method: "POST",
|
|
443
|
+
headers: {
|
|
444
|
+
"Content-Type": "application/json",
|
|
445
|
+
Authorization: `Bearer ${apiKey}`,
|
|
446
|
+
},
|
|
447
|
+
body: JSON.stringify(body),
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
if (!response.ok) {
|
|
451
|
+
const errorBody = await response.text().catch(() => "");
|
|
452
|
+
throw new Error(`Cohere API error (${response.status}): ${response.statusText}. ${errorBody}`);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (isStreaming && response.body && typeof response.body.getReader === "function") {
|
|
456
|
+
const reader = response.body.getReader();
|
|
457
|
+
const decoder = new TextDecoder();
|
|
458
|
+
let buffer = "";
|
|
459
|
+
let fullText = "";
|
|
460
|
+
|
|
461
|
+
while (true) {
|
|
462
|
+
const { done, value } = await reader.read();
|
|
463
|
+
if (done) break;
|
|
464
|
+
buffer += decoder.decode(value, { stream: true });
|
|
465
|
+
|
|
466
|
+
const lines = buffer.split("\n");
|
|
467
|
+
buffer = lines.pop();
|
|
468
|
+
|
|
469
|
+
for (const line of lines) {
|
|
470
|
+
const trimmed = line.trim();
|
|
471
|
+
if (!trimmed) continue;
|
|
472
|
+
if (trimmed.startsWith("data: ")) {
|
|
473
|
+
try {
|
|
474
|
+
const data = JSON.parse(trimmed.slice(6));
|
|
475
|
+
const content = data?.delta?.message?.content?.text || "";
|
|
476
|
+
if (content) {
|
|
477
|
+
onToken(content);
|
|
478
|
+
fullText += content;
|
|
479
|
+
}
|
|
480
|
+
} catch (e) {
|
|
481
|
+
// Ignore partial JSON
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
// Flush remaining
|
|
487
|
+
if (buffer && buffer.startsWith("data: ")) {
|
|
488
|
+
try {
|
|
489
|
+
const data = JSON.parse(buffer.slice(6));
|
|
490
|
+
const content = data?.delta?.message?.content?.text || "";
|
|
491
|
+
if (content) {
|
|
492
|
+
onToken(content);
|
|
493
|
+
fullText += content;
|
|
494
|
+
}
|
|
495
|
+
} catch (e) {}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (!fullText) throw new Error("Cohere returned empty response");
|
|
499
|
+
return { text: fullText, provider: "cohere", model };
|
|
500
|
+
} else {
|
|
501
|
+
const data = await response.json();
|
|
502
|
+
const text = data?.message?.content?.[0]?.text;
|
|
503
|
+
|
|
504
|
+
if (!text) throw new Error("Cohere returned empty response");
|
|
505
|
+
return { text, provider: "cohere", model };
|
|
506
|
+
}
|
|
507
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════
|
|
2
|
+
// AETHER AI CLI — xAI/Grok API Provider
|
|
3
|
+
// ═══════════════════════════════════════════════════════════
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Sends a prompt to the xAI (Grok) API.
|
|
7
|
+
* @param {string} prompt - The user message
|
|
8
|
+
* @param {string} systemPrompt - System prompt for the mode
|
|
9
|
+
* @param {string} apiKey - xAI API key
|
|
10
|
+
* @param {string} [model='grok-2'] - Model name
|
|
11
|
+
* @returns {Promise<{ text: string, provider: string, model: string }>}
|
|
12
|
+
*/
|
|
13
|
+
export async function callXai(prompt, systemPrompt, apiKey, model = "grok-2") {
|
|
14
|
+
const url = "https://api.x.ai/v1/chat/completions";
|
|
15
|
+
|
|
16
|
+
const body = {
|
|
17
|
+
model,
|
|
18
|
+
messages: [
|
|
19
|
+
{ role: "system", content: systemPrompt },
|
|
20
|
+
{ role: "user", content: prompt },
|
|
21
|
+
],
|
|
22
|
+
temperature: 0.7,
|
|
23
|
+
max_tokens: 4096,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const response = await fetch(url, {
|
|
27
|
+
method: "POST",
|
|
28
|
+
headers: {
|
|
29
|
+
"Content-Type": "application/json",
|
|
30
|
+
Authorization: `Bearer ${apiKey}`,
|
|
31
|
+
},
|
|
32
|
+
body: JSON.stringify(body),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
if (!response.ok) {
|
|
36
|
+
const errorBody = await response.text().catch(() => "");
|
|
37
|
+
throw new Error(
|
|
38
|
+
`xAI API error (${response.status}): ${response.statusText}. ${errorBody}`
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const data = await response.json();
|
|
43
|
+
const text = data?.choices?.[0]?.message?.content;
|
|
44
|
+
|
|
45
|
+
if (!text) {
|
|
46
|
+
throw new Error("xAI API returned empty response");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return { text, provider: "xai", model };
|
|
50
|
+
}
|