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.
@@ -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
+ }