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,46 @@
1
+ {
2
+ "name": "@krishivpb60/aether-ai-cli",
3
+ "version": "1.1.4",
4
+ "description": "Aether Core AI — A cyberpunk command-line AI assistant with multi-mode reasoning, 12-node failover mesh, file context injection, and offline fallbacks.",
5
+ "main": "src/cli.js",
6
+ "bin": {
7
+ "aether": "bin/aether.js"
8
+ },
9
+ "type": "module",
10
+ "scripts": {
11
+ "start": "node bin/aether.js",
12
+ "test": "node --test"
13
+ },
14
+ "keywords": [
15
+ "ai",
16
+ "cli",
17
+ "gemini",
18
+ "grok",
19
+ "xai",
20
+ "chatbot",
21
+ "aether",
22
+ "cyberpunk",
23
+ "terminal",
24
+ "assistant"
25
+ ],
26
+ "author": "Krishiv PB <krylobloxyt@gmail.com> (https://github.com/Krylo-60)",
27
+ "license": "MIT",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/Krylo-60/aether-ai-cli.git"
31
+ },
32
+ "homepage": "https://github.com/Krylo-60/aether-ai-cli#readme",
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "engines": {
37
+ "node": ">=18.0.0"
38
+ },
39
+ "dependencies": {
40
+ "chalk": "^5.3.0",
41
+ "commander": "^12.1.0",
42
+ "marked": "^14.0.0",
43
+ "marked-terminal": "^7.2.0",
44
+ "ora": "^8.1.0"
45
+ }
46
+ }
@@ -0,0 +1,179 @@
1
+ // ═══════════════════════════════════════════════════════════
2
+ // AETHER AI CLI — Local Fallback Engine
3
+ // Math Solver + Krylo Companion Bot
4
+ // ═══════════════════════════════════════════════════════════
5
+
6
+ const KRYLO_REPLIES = [
7
+ "Affirmative, commander. Systems are running at peak cybernetic capacity.",
8
+ "Neon grids initialized. Matrix color modulates are at nominal density.",
9
+ "Warning: Solar flare activity detected. Detuning audio synth harmonics by 18.4% to compensate.",
10
+ "Neural nodes synchronized. Analyzing the portfolio's glassmorphic boundaries.",
11
+ "I am Krylo, your holographic companion terminal. Ready to warp index nodes.",
12
+ "Ecosystem diagnostics complete. 0 memory leaks, 100% premium responsive UI."
13
+ ];
14
+
15
+ /**
16
+ * Detects if a prompt is a pure mathematical expression.
17
+ * Supports basic operators, parentheses, standard math functions, and constants.
18
+ * @param {string} prompt - The user prompt
19
+ * @returns {string|null} The cleaned expression or null
20
+ */
21
+ export function detectMathExpression(prompt) {
22
+ const clean = prompt.replace(/\s+/g, "").toLowerCase();
23
+
24
+ // Check if it's a word-based status query
25
+ if (clean === "status" || clean === "hud") return null;
26
+
27
+ // Strip allowed function and constant words
28
+ const structure = clean.replace(/sin|cos|tan|log|ln|sqrt|pi|e|abs/g, "");
29
+
30
+ // Must be composed of valid math characters, AND contain either a math operator or an active function call
31
+ if (/^[0-9+\-*/().%^]+$/.test(structure)) {
32
+ const hasOperator = /[+\-*/%^]/.test(structure);
33
+ const hasFunction = /(sin|cos|tan|log|ln|sqrt|abs)\(/.test(clean);
34
+ if (hasOperator || hasFunction) {
35
+ return clean;
36
+ }
37
+ }
38
+ return null;
39
+ }
40
+
41
+ /**
42
+ * Safely evaluates a mathematical expression locally.
43
+ * Supports trig, square root, natural/base-10 logs, absolute values, pi, and e.
44
+ * @param {string} expression - A sanitized math expression
45
+ * @returns {{ text: string, type: string }|null}
46
+ */
47
+ export function solveMath(expression) {
48
+ if (!expression) return null;
49
+ try {
50
+ const clean = expression.replace(/\s+/g, "").toLowerCase();
51
+
52
+ // Validate character structure
53
+ const structure = clean.replace(/sin|cos|tan|log|ln|sqrt|pi|e|abs/g, "");
54
+ if (!/^[0-9+\-*/().%^]+$/.test(structure)) return null;
55
+
56
+ // Convert terms to JavaScript Math equivalents
57
+ let jsExpr = clean
58
+ .replace(/\^/g, "**")
59
+ .replace(/sin\(/g, "Math.sin(")
60
+ .replace(/cos\(/g, "Math.cos(")
61
+ .replace(/tan\(/g, "Math.tan(")
62
+ .replace(/log\(/g, "Math.log10(")
63
+ .replace(/ln\(/g, "Math.log(")
64
+ .replace(/sqrt\(/g, "Math.sqrt(")
65
+ .replace(/abs\(/g, "Math.abs(")
66
+ .replace(/\bpi\b/g, "Math.PI")
67
+ .replace(/\be\b/g, "Math.E");
68
+
69
+ const result = Function(`"use strict"; return (${jsExpr})`)();
70
+ if (typeof result === "number" && !isNaN(result) && isFinite(result)) {
71
+ return {
72
+ text: [
73
+ "🤖 [LOCAL MATH SOLVER]",
74
+ ` Expression: ${expression}`,
75
+ ` Result: ${Number(result.toFixed(6))}`,
76
+ ].join("\n"),
77
+ type: "local-math",
78
+ };
79
+ }
80
+ } catch {
81
+ // Not a valid expression
82
+ }
83
+ return null;
84
+ }
85
+
86
+ /**
87
+ * Starts a mainframe security bypass mini-game.
88
+ * @returns {{ text: string, type: string }}
89
+ */
90
+ export function runMainframeHack() {
91
+ return {
92
+ text: [
93
+ "⚡ [LOCAL TERMINAL SECURITY BYPASS GAME]",
94
+ " MAINFRAME HACK PROTOCOL LOADED.",
95
+ " ────────────────────────────────────────",
96
+ " Objective: Bypass security by guessing the 4-digit PIN (digits 0-9).",
97
+ " For each guess, you will get feedback:",
98
+ " • 'Hit' - correct digit in correct position.",
99
+ " • 'Close' - correct digit but in wrong position.",
100
+ " You have 6 attempts before security lock-out.",
101
+ " ",
102
+ " Type `/guess <number>` to input breach code (e.g. /guess 2941)",
103
+ " To abort, type `/abort`.",
104
+ " ────────────────────────────────────────",
105
+ ].join("\n"),
106
+ type: "mainframe-game",
107
+ };
108
+ }
109
+
110
+ /**
111
+ * Generates a local Krylo companion reply based on keywords.
112
+ * @param {string} prompt - The user prompt
113
+ * @returns {{ text: string, type: string }}
114
+ */
115
+ export function generateKryloReply(prompt) {
116
+ const clean = prompt.toLowerCase();
117
+
118
+ if (clean.includes("help") || clean.includes("shortcut") || clean.includes("command")) {
119
+ return {
120
+ text: [
121
+ "💡 [SYSTEM DECK CHEAT SHEET]",
122
+ " • Use `Ctrl + K` to open the Portal Search.",
123
+ " • Use `Ctrl + Shift + L` to open the Links Directory.",
124
+ " • Trigger Konami Code `↑↑↓↓←→←→BA` to launch Matrix mode!",
125
+ " • Type `/mode <name>` to switch reasoning modes.",
126
+ " • Type `/attach <file>` to inject file context.",
127
+ " • Type `/export` to save the conversation.",
128
+ ].join("\n"),
129
+ type: "krylo-local",
130
+ };
131
+ }
132
+
133
+ if (clean.includes("status") || clean.includes("hud") || clean.includes("cpu") || clean.includes("ping") || clean.includes("diagnostics")) {
134
+ return {
135
+ text: [
136
+ "📊 [LIVE DIAGNOSTIC READOUT]",
137
+ " • CPU Core Load: 15.4% (Optimized)",
138
+ " • Ping Latency: 12ms (Hyper-Fast)",
139
+ " • Memory Usage: 247MB / 8192MB",
140
+ " • Canvas Sparklines: Active and tracking vectors",
141
+ " • Failover Mesh: All 12 nodes standing by",
142
+ ].join("\n"),
143
+ type: "krylo-local",
144
+ };
145
+ }
146
+
147
+ if (clean.includes("matrix") || clean.includes("rain") || clean.includes("color")) {
148
+ return {
149
+ text: [
150
+ "⚡ [NEURAL GRIDS MODULATION]",
151
+ " • Five stream channels active:",
152
+ " Classic Green, Cyber Cyan, Neon Purple,",
153
+ " Overdrive Red, Golden Matrix.",
154
+ " • Detuned Web Audio frequency active.",
155
+ " • Matrix rain density: 94.2%",
156
+ ].join("\n"),
157
+ type: "krylo-local",
158
+ };
159
+ }
160
+
161
+ if (clean.includes("who") || clean.includes("name") || clean.includes("creator")) {
162
+ return {
163
+ text: [
164
+ "🤖 [HOLOGRAPHIC COMPANION PROTOCOL]",
165
+ " • Identification: Krylo (Nexus Companion)",
166
+ " • Purpose: Pair-programming assistant & Commander companion",
167
+ " • Creator: Krishiv PB — The Master Coder",
168
+ " • Version: Aether Core AI v110 — Fusion Build",
169
+ ].join("\n"),
170
+ type: "krylo-local",
171
+ };
172
+ }
173
+
174
+ const index = Math.floor(Math.random() * KRYLO_REPLIES.length);
175
+ return {
176
+ text: `🤖 [KRYLO TERMINAL RESPONSE]\n ${KRYLO_REPLIES[index]}`,
177
+ type: "krylo-local",
178
+ };
179
+ }
@@ -0,0 +1,87 @@
1
+ // ═══════════════════════════════════════════════════════════
2
+ // AETHER AI CLI — Google Gemini API Provider
3
+ // ═══════════════════════════════════════════════════════════
4
+
5
+ const BASE_URL = "https://generativelanguage.googleapis.com/v1beta/models";
6
+ const MAX_CONTINUATIONS = 3;
7
+
8
+ /**
9
+ * Sends a prompt to the Google Gemini API.
10
+ * Handles continuation if finishReason is MAX_TOKENS.
11
+ * @param {string} prompt - The user message
12
+ * @param {string} systemPrompt - System prompt for the mode
13
+ * @param {string} apiKey - Google API key
14
+ * @param {string} [model='gemini-2.5-flash'] - Model name
15
+ * @returns {Promise<{ text: string, provider: string, model: string }>}
16
+ */
17
+ export async function callGemini(prompt, systemPrompt, apiKey, model = "gemini-2.5-flash") {
18
+ let fullText = "";
19
+ let currentPrompt = prompt;
20
+ let continuations = 0;
21
+
22
+ while (continuations <= MAX_CONTINUATIONS) {
23
+ const url = `${BASE_URL}/${model}:generateContent?key=${apiKey}`;
24
+
25
+ const body = {
26
+ systemInstruction: {
27
+ parts: [{ text: systemPrompt }],
28
+ },
29
+ contents: [
30
+ {
31
+ role: "user",
32
+ parts: [{ text: currentPrompt }],
33
+ },
34
+ ],
35
+ generationConfig: {
36
+ temperature: 0.7,
37
+ maxOutputTokens: 8192,
38
+ },
39
+ };
40
+
41
+ const response = await fetch(url, {
42
+ method: "POST",
43
+ headers: { "Content-Type": "application/json" },
44
+ body: JSON.stringify(body),
45
+ });
46
+
47
+ if (!response.ok) {
48
+ const errorBody = await response.text().catch(() => "");
49
+ throw new Error(
50
+ `Gemini API error (${response.status}): ${response.statusText}. ${errorBody}`
51
+ );
52
+ }
53
+
54
+ const data = await response.json();
55
+
56
+ // Check for blocked content
57
+ if (data.promptFeedback?.blockReason) {
58
+ throw new Error(`Content blocked: ${data.promptFeedback.blockReason}`);
59
+ }
60
+
61
+ const candidate = data.candidates?.[0];
62
+ if (!candidate) {
63
+ throw new Error("Gemini API returned no candidates");
64
+ }
65
+
66
+ const chunkText = candidate.content?.parts
67
+ ?.map((p) => p.text)
68
+ .filter(Boolean)
69
+ .join("") || "";
70
+
71
+ fullText += chunkText;
72
+
73
+ // Check if the response was cut short
74
+ if (candidate.finishReason === "MAX_TOKENS" && continuations < MAX_CONTINUATIONS) {
75
+ continuations++;
76
+ currentPrompt = "Continue your previous response from exactly where you left off.";
77
+ } else {
78
+ break;
79
+ }
80
+ }
81
+
82
+ if (!fullText.trim()) {
83
+ throw new Error("Gemini API returned empty response");
84
+ }
85
+
86
+ return { text: fullText, provider: "google", model };
87
+ }
@@ -0,0 +1,203 @@
1
+ // ═══════════════════════════════════════════════════════════
2
+ // AETHER AI CLI — Universal Provider Registry
3
+ // Supports ANY OpenAI-compatible API + custom providers
4
+ // ═══════════════════════════════════════════════════════════
5
+
6
+ /**
7
+ * Registry of all supported AI providers.
8
+ * Each provider defines its API format, base URL, default model, and pricing tier.
9
+ *
10
+ * Providers with `format: "openai"` use the standard /v1/chat/completions endpoint.
11
+ * Providers with `format: "custom"` have their own handler in dedicated files.
12
+ */
13
+ export const PROVIDERS = {
14
+ // ── Free Tier Providers ─────────────────────────────────
15
+ groq: {
16
+ name: "Groq",
17
+ key: "GROQ_API_KEY",
18
+ format: "openai",
19
+ baseUrl: "https://api.groq.com/openai/v1/chat/completions",
20
+ defaultModel: "llama-3.3-70b-versatile",
21
+ models: ["llama-3.3-70b-versatile", "llama-3.1-8b-instant", "mixtral-8x7b-32768", "gemma2-9b-it"],
22
+ tier: "free",
23
+ description: "Ultra-fast inference on Llama, Mixtral, Gemma (generous free tier)",
24
+ },
25
+ together: {
26
+ name: "Together AI",
27
+ key: "TOGETHER_API_KEY",
28
+ format: "openai",
29
+ baseUrl: "https://api.together.xyz/v1/chat/completions",
30
+ defaultModel: "meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo",
31
+ models: ["meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo", "mistralai/Mixtral-8x7B-Instruct-v0.1", "Qwen/Qwen2.5-72B-Instruct-Turbo"],
32
+ tier: "free",
33
+ description: "Open-source models with free credits on signup",
34
+ },
35
+ cerebras: {
36
+ name: "Cerebras",
37
+ key: "CEREBRAS_API_KEY",
38
+ format: "openai",
39
+ baseUrl: "https://api.cerebras.ai/v1/chat/completions",
40
+ defaultModel: "llama-3.3-70b",
41
+ models: ["llama-3.3-70b", "llama-3.1-8b"],
42
+ tier: "free",
43
+ description: "Fastest inference engine — free tier available",
44
+ },
45
+
46
+ // ── OpenAI-Compatible Providers ─────────────────────────
47
+ openai: {
48
+ name: "OpenAI",
49
+ key: "OPENAI_API_KEY",
50
+ format: "openai",
51
+ baseUrl: "https://api.openai.com/v1/chat/completions",
52
+ defaultModel: "gpt-4o",
53
+ models: ["gpt-4o", "gpt-4o-mini", "gpt-4-turbo", "gpt-3.5-turbo", "o1-mini", "o3-mini"],
54
+ tier: "paid",
55
+ description: "GPT-4o, GPT-4, o1 reasoning models",
56
+ },
57
+ mistral: {
58
+ name: "Mistral AI",
59
+ key: "MISTRAL_API_KEY",
60
+ format: "openai",
61
+ baseUrl: "https://api.mistral.ai/v1/chat/completions",
62
+ defaultModel: "mistral-large-latest",
63
+ models: ["mistral-large-latest", "mistral-medium-latest", "mistral-small-latest", "open-mistral-nemo"],
64
+ tier: "paid",
65
+ description: "Mistral Large, Medium, and open-weight models",
66
+ },
67
+ fireworks: {
68
+ name: "Fireworks AI",
69
+ key: "FIREWORKS_API_KEY",
70
+ format: "openai",
71
+ baseUrl: "https://api.fireworks.ai/inference/v1/chat/completions",
72
+ defaultModel: "accounts/fireworks/models/llama-v3p1-70b-instruct",
73
+ models: ["accounts/fireworks/models/llama-v3p1-70b-instruct", "accounts/fireworks/models/mixtral-8x7b-instruct"],
74
+ tier: "free",
75
+ description: "Fast open-source model hosting with free tier",
76
+ },
77
+ openrouter: {
78
+ name: "OpenRouter",
79
+ key: "OPENROUTER_API_KEY",
80
+ format: "openai",
81
+ baseUrl: "https://openrouter.ai/api/v1/chat/completions",
82
+ defaultModel: "meta-llama/llama-3.1-70b-instruct:free",
83
+ models: ["meta-llama/llama-3.1-70b-instruct:free", "google/gemini-2.5-flash-preview:free", "mistralai/mistral-7b-instruct:free", "anthropic/claude-sonnet-4", "openai/gpt-4o"],
84
+ tier: "free+paid",
85
+ description: "Gateway to 200+ models — many free options available",
86
+ },
87
+ deepseek: {
88
+ name: "DeepSeek",
89
+ key: "DEEPSEEK_API_KEY",
90
+ format: "openai",
91
+ baseUrl: "https://api.deepseek.com/v1/chat/completions",
92
+ defaultModel: "deepseek-chat",
93
+ models: ["deepseek-chat", "deepseek-reasoner"],
94
+ tier: "paid",
95
+ description: "DeepSeek-V3 and R1 reasoning model",
96
+ },
97
+ perplexity: {
98
+ name: "Perplexity",
99
+ key: "PERPLEXITY_API_KEY",
100
+ format: "openai",
101
+ baseUrl: "https://api.perplexity.ai/chat/completions",
102
+ defaultModel: "sonar",
103
+ models: ["sonar", "sonar-pro", "sonar-reasoning"],
104
+ tier: "paid",
105
+ description: "Search-augmented AI with real-time web access",
106
+ },
107
+
108
+ // ── Custom Format Providers ─────────────────────────────
109
+ xai: {
110
+ name: "xAI Grok",
111
+ key: "XAI_API_KEY",
112
+ format: "openai",
113
+ baseUrl: "https://api.x.ai/v1/chat/completions",
114
+ defaultModel: "grok-2",
115
+ models: ["grok-2", "grok-2-mini"],
116
+ tier: "paid",
117
+ description: "Grok-2 by xAI — witty, uncensored",
118
+ },
119
+ google: {
120
+ name: "Google Gemini",
121
+ key: "GOOGLE_API_KEY",
122
+ format: "custom-google",
123
+ baseUrl: "https://generativelanguage.googleapis.com/v1beta/models",
124
+ defaultModel: "gemini-2.5-flash",
125
+ models: ["gemini-2.5-flash", "gemini-2.5-pro", "gemini-2.0-flash", "gemini-1.5-pro"],
126
+ tier: "free+paid",
127
+ description: "Gemini 2.5 Flash/Pro — free tier with generous limits",
128
+ },
129
+ anthropic: {
130
+ name: "Anthropic Claude",
131
+ key: "ANTHROPIC_API_KEY",
132
+ format: "custom-anthropic",
133
+ baseUrl: "https://api.anthropic.com/v1/messages",
134
+ defaultModel: "claude-sonnet-4-20250514",
135
+ models: ["claude-sonnet-4-20250514", "claude-3-5-haiku-20241022", "claude-3-5-sonnet-20241022"],
136
+ tier: "paid",
137
+ description: "Claude Sonnet 4, Haiku — premium reasoning",
138
+ },
139
+ cohere: {
140
+ name: "Cohere",
141
+ key: "COHERE_API_KEY",
142
+ format: "custom-cohere",
143
+ baseUrl: "https://api.cohere.com/v2/chat",
144
+ defaultModel: "command-r-plus",
145
+ models: ["command-r-plus", "command-r", "command-light"],
146
+ tier: "free+paid",
147
+ description: "Command R+ — free tier for developers",
148
+ },
149
+ };
150
+
151
+ /**
152
+ * Gets a flat list of all provider keys that can be configured.
153
+ * @returns {string[]}
154
+ */
155
+ export function getAllConfigKeys() {
156
+ const keys = new Set();
157
+ for (const p of Object.values(PROVIDERS)) {
158
+ keys.add(p.key);
159
+ }
160
+ // Also include multi-key support for Google
161
+ keys.add("GOOGLE_API_KEYS");
162
+ return [...keys];
163
+ }
164
+
165
+ /**
166
+ * Gets a provider by its config key name (e.g., "OPENAI_API_KEY" → openai provider).
167
+ * @param {string} configKey
168
+ * @returns {object|null}
169
+ */
170
+ export function getProviderByKey(configKey) {
171
+ for (const [id, p] of Object.entries(PROVIDERS)) {
172
+ if (p.key === configKey) return { id, ...p };
173
+ }
174
+ return null;
175
+ }
176
+
177
+ /**
178
+ * Gets all providers that have a valid key in the given config.
179
+ * @param {object} config - Config object with API keys
180
+ * @returns {Array<{ id: string, provider: object, apiKey: string }>}
181
+ */
182
+ export function getActiveProviders(config) {
183
+ const active = [];
184
+ for (const [id, provider] of Object.entries(PROVIDERS)) {
185
+ const apiKey = config[provider.key];
186
+ if (apiKey) {
187
+ active.push({ id, provider, apiKey });
188
+ }
189
+ }
190
+ return active;
191
+ }
192
+
193
+ /**
194
+ * Groups providers by pricing tier for display.
195
+ * @returns {{ free: object[], paid: object[], mixed: object[] }}
196
+ */
197
+ export function getProvidersByTier() {
198
+ const result = { free: [], "free+paid": [], paid: [] };
199
+ for (const [id, provider] of Object.entries(PROVIDERS)) {
200
+ result[provider.tier]?.push({ id, ...provider });
201
+ }
202
+ return result;
203
+ }
@@ -0,0 +1,114 @@
1
+ // ═══════════════════════════════════════════════════════════
2
+ // AETHER AI CLI — Universal AI Router with Failover Mesh
3
+ // Routes through ALL configured providers automatically
4
+ // ═══════════════════════════════════════════════════════════
5
+
6
+ import { detectMathExpression, solveMath, generateKryloReply } from "./fallback.js";
7
+ import { PROVIDERS, getActiveProviders } from "./providers.js";
8
+ import {
9
+ callOpenAICompatible,
10
+ callGoogleGemini,
11
+ callAnthropic,
12
+ callCohere,
13
+ } from "./universal.js";
14
+
15
+ /**
16
+ * Routes a prompt through the universal AI failover mesh.
17
+ *
18
+ * Priority Order:
19
+ * 1. Local math solver (if pure math expression)
20
+ * 2. All configured providers, in order of priority
21
+ * 3. Google key rotation (if multiple keys)
22
+ * 4. Krylo companion fallback (if everything else fails)
23
+ *
24
+ * @param {string} prompt - The user prompt
25
+ * @param {string} systemPrompt - The mode system prompt
26
+ * @param {object} config - Flat config object with all API keys
27
+ * @returns {Promise<{ text: string, provider: string, model?: string, node: number, type?: string }>}
28
+ */
29
+ export async function routePrompt(prompt, systemPrompt, config, onToken, history = []) {
30
+ // ── Node 0: Local Math Solver ───────────────────────────
31
+ const mathExpr = detectMathExpression(prompt);
32
+ if (mathExpr) {
33
+ const mathResult = solveMath(mathExpr);
34
+ if (mathResult) {
35
+ return { ...mathResult, provider: "local", node: 0 };
36
+ }
37
+ }
38
+
39
+ // ── Gather all active providers ─────────────────────────
40
+ const active = getActiveProviders(config);
41
+
42
+ // Add extra Google keys for rotation
43
+ const googleExtraKeys = config.GOOGLE_API_KEYS;
44
+ if (googleExtraKeys) {
45
+ const extras = googleExtraKeys.split(",").map((k) => k.trim()).filter(Boolean);
46
+ for (const key of extras) {
47
+ // Avoid duplicates
48
+ if (!active.some((a) => a.id === "google" && a.apiKey === key)) {
49
+ active.push({ id: "google-extra", provider: PROVIDERS.google, apiKey: key });
50
+ }
51
+ }
52
+ }
53
+
54
+ // ── No providers configured → Krylo ────────────────────
55
+ if (active.length === 0) {
56
+ const kryloReply = generateKryloReply(prompt);
57
+ return { ...kryloReply, provider: "krylo-fallback", node: 0 };
58
+ }
59
+
60
+ // ── Try each provider in order ──────────────────────────
61
+ const errors = [];
62
+ let nodeIndex = 1;
63
+
64
+ for (const { id, provider, apiKey } of active) {
65
+ try {
66
+ const model = config[`${id.toUpperCase()}_MODEL`] || provider.defaultModel;
67
+ let result;
68
+
69
+ switch (provider.format) {
70
+ case "openai":
71
+ result = await callOpenAICompatible(
72
+ prompt, systemPrompt, apiKey,
73
+ provider.baseUrl, model, provider.name,
74
+ onToken, history
75
+ );
76
+ break;
77
+
78
+ case "custom-google":
79
+ result = await callGoogleGemini(prompt, systemPrompt, apiKey, model, onToken, history);
80
+ break;
81
+
82
+ case "custom-anthropic":
83
+ result = await callAnthropic(prompt, systemPrompt, apiKey, model, onToken, history);
84
+ break;
85
+
86
+ case "custom-cohere":
87
+ result = await callCohere(prompt, systemPrompt, apiKey, model, onToken, history);
88
+ break;
89
+
90
+ default:
91
+ // Treat unknown formats as OpenAI-compatible
92
+ result = await callOpenAICompatible(
93
+ prompt, systemPrompt, apiKey,
94
+ provider.baseUrl, model, provider.name,
95
+ onToken, history
96
+ );
97
+ }
98
+
99
+ return { ...result, node: nodeIndex };
100
+ } catch (err) {
101
+ errors.push(`[Node ${nodeIndex} ${provider.name}] ${err.message}`);
102
+ nodeIndex++;
103
+ }
104
+ }
105
+
106
+ // ── Final Fallback: Krylo Companion ─────────────────────
107
+ const kryloReply = generateKryloReply(prompt);
108
+ return {
109
+ ...kryloReply,
110
+ provider: "krylo-fallback",
111
+ node: 0,
112
+ errors,
113
+ };
114
+ }