wispy-cli 2.4.4 → 2.5.1
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.
- package/bin/wispy.mjs +131 -14
- package/core/config.mjs +64 -12
- package/core/onboarding.mjs +147 -18
- package/core/providers.mjs +51 -7
- package/package.json +1 -1
package/bin/wispy.mjs
CHANGED
|
@@ -131,7 +131,10 @@ ${_bold("Cron & Automation:")}
|
|
|
131
131
|
${_cyan("wispy cron start")} Start scheduler
|
|
132
132
|
|
|
133
133
|
${_bold("Config & Maintenance:")}
|
|
134
|
-
${_cyan("wispy setup")} Configure wispy interactively
|
|
134
|
+
${_cyan("wispy setup")} Configure wispy interactively ${_dim("(29 AI providers)")}
|
|
135
|
+
${_cyan("wispy model")} Show / switch AI model
|
|
136
|
+
${_cyan("wispy model list")} List available models per provider
|
|
137
|
+
${_cyan("wispy model set <p:model>")} Switch model (e.g. xai:grok-3)
|
|
135
138
|
${_cyan("wispy update")} Update to latest version
|
|
136
139
|
${_cyan("wispy migrate")} Import from OpenClaw
|
|
137
140
|
${_cyan("wispy doctor")} Check system health
|
|
@@ -500,13 +503,32 @@ if (args[0] === "doctor") {
|
|
|
500
503
|
// 3. API key configured
|
|
501
504
|
if (config) {
|
|
502
505
|
const envMap = {
|
|
503
|
-
google:
|
|
504
|
-
anthropic:
|
|
505
|
-
openai:
|
|
506
|
-
groq:
|
|
507
|
-
openrouter:
|
|
508
|
-
deepseek:
|
|
509
|
-
|
|
506
|
+
google: ["GOOGLE_AI_KEY", "GOOGLE_GENERATIVE_AI_KEY", "GEMINI_API_KEY"],
|
|
507
|
+
anthropic: ["ANTHROPIC_API_KEY"],
|
|
508
|
+
openai: ["OPENAI_API_KEY"],
|
|
509
|
+
groq: ["GROQ_API_KEY"],
|
|
510
|
+
openrouter: ["OPENROUTER_API_KEY"],
|
|
511
|
+
deepseek: ["DEEPSEEK_API_KEY"],
|
|
512
|
+
xai: ["XAI_API_KEY"],
|
|
513
|
+
mistral: ["MISTRAL_API_KEY"],
|
|
514
|
+
together: ["TOGETHER_API_KEY"],
|
|
515
|
+
nvidia: ["NVIDIA_API_KEY"],
|
|
516
|
+
kimi: ["MOONSHOT_API_KEY", "KIMI_API_KEY"],
|
|
517
|
+
minimax: ["MINIMAX_API_KEY"],
|
|
518
|
+
chutes: ["CHUTES_API_KEY"],
|
|
519
|
+
venice: ["VENICE_API_KEY"],
|
|
520
|
+
huggingface: ["HF_TOKEN", "HUGGINGFACE_API_KEY"],
|
|
521
|
+
cloudflare: ["CF_API_TOKEN"],
|
|
522
|
+
volcengine: ["VOLCENGINE_API_KEY", "ARK_API_KEY"],
|
|
523
|
+
byteplus: ["BYTEPLUS_API_KEY"],
|
|
524
|
+
zai: ["ZAI_API_KEY", "GLM_API_KEY", "ZHIPU_API_KEY"],
|
|
525
|
+
dashscope: ["DASHSCOPE_API_KEY"],
|
|
526
|
+
xiaomi: ["XIAOMI_API_KEY"],
|
|
527
|
+
vercelai: ["VERCEL_AI_TOKEN"],
|
|
528
|
+
litellm: ["LITELLM_API_KEY"],
|
|
529
|
+
ollama: null, // no key
|
|
530
|
+
vllm: null, // no key
|
|
531
|
+
sglang: null, // no key
|
|
510
532
|
};
|
|
511
533
|
// Support both old (config.provider) and new (config.providers) format
|
|
512
534
|
const providers = config.providers ? Object.keys(config.providers) : (config.provider ? [config.provider] : []);
|
|
@@ -514,12 +536,13 @@ if (args[0] === "doctor") {
|
|
|
514
536
|
check("AI provider", false, "no provider configured — run 'wispy setup provider'");
|
|
515
537
|
} else {
|
|
516
538
|
for (const provider of providers) {
|
|
517
|
-
const
|
|
518
|
-
if (
|
|
539
|
+
const envKeys = envMap[provider];
|
|
540
|
+
if (envKeys === null) {
|
|
519
541
|
check(`AI provider (${provider})`, true, "no key needed");
|
|
520
|
-
} else if (
|
|
521
|
-
const key = config.providers?.[provider]?.apiKey || config.apiKey
|
|
522
|
-
|
|
542
|
+
} else if (envKeys) {
|
|
543
|
+
const key = config.providers?.[provider]?.apiKey || config.apiKey
|
|
544
|
+
|| envKeys.reduce((found, k) => found || process.env[k], null);
|
|
545
|
+
check(`API key (${provider})`, !!key && key.length > 8, key ? "configured" : `set env var or run 'wispy setup provider'`);
|
|
523
546
|
} else {
|
|
524
547
|
check(`AI provider (${provider})`, false, `unknown provider`);
|
|
525
548
|
}
|
|
@@ -760,6 +783,96 @@ if (args[0] === "update") {
|
|
|
760
783
|
process.exit(0);
|
|
761
784
|
}
|
|
762
785
|
|
|
786
|
+
// ── model sub-command ─────────────────────────────────────────────────────────
|
|
787
|
+
if (args[0] === "model") {
|
|
788
|
+
const { loadConfig, saveConfig, PROVIDERS } = await import(
|
|
789
|
+
path.join(__dirname, "..", "core", "config.mjs")
|
|
790
|
+
);
|
|
791
|
+
|
|
792
|
+
const KNOWN_MODELS = {
|
|
793
|
+
google: ["gemini-2.5-flash", "gemini-2.5-pro", "gemini-2.0-flash", "gemini-1.5-pro"],
|
|
794
|
+
anthropic: ["claude-sonnet-4-20250514", "claude-opus-4-6", "claude-haiku-3.5", "claude-3-5-sonnet-20241022"],
|
|
795
|
+
openai: ["gpt-4o", "gpt-4o-mini", "gpt-4.1", "gpt-4.1-mini", "o4-mini", "o3"],
|
|
796
|
+
xai: ["grok-3", "grok-3-mini", "grok-2-1212"],
|
|
797
|
+
mistral: ["mistral-large-latest", "mistral-small-latest", "codestral-latest", "open-mistral-nemo"],
|
|
798
|
+
groq: ["llama-3.3-70b-versatile", "llama-3.1-8b-instant", "mixtral-8x7b-32768", "gemma2-9b-it"],
|
|
799
|
+
deepseek: ["deepseek-chat", "deepseek-reasoner"],
|
|
800
|
+
together: ["meta-llama/Llama-3.3-70B-Instruct-Turbo", "Qwen/Qwen2.5-72B-Instruct-Turbo"],
|
|
801
|
+
openrouter: ["anthropic/claude-sonnet-4-20250514", "openai/gpt-4o", "google/gemini-2.5-flash"],
|
|
802
|
+
chutes: ["deepseek-ai/DeepSeek-V3-0324", "deepseek-ai/DeepSeek-R1"],
|
|
803
|
+
nvidia: ["meta/llama-3.3-70b-instruct", "nvidia/llama-3.1-nemotron-70b-instruct"],
|
|
804
|
+
kimi: ["moonshot-v1-8k", "moonshot-v1-32k", "moonshot-v1-128k"],
|
|
805
|
+
zai: ["glm-4-flash", "glm-4-plus", "glm-z1-flash"],
|
|
806
|
+
dashscope: ["qwen-max", "qwen-plus", "qwen-turbo"],
|
|
807
|
+
volcengine: ["doubao-pro-32k", "doubao-lite-32k"],
|
|
808
|
+
ollama: ["llama3.2", "llama3.1", "qwen2.5", "phi3", "mistral"],
|
|
809
|
+
};
|
|
810
|
+
|
|
811
|
+
const sub = args[1];
|
|
812
|
+
const config = await loadConfig();
|
|
813
|
+
const configuredProviders = config.providers ? Object.keys(config.providers) : (config.provider ? [config.provider] : []);
|
|
814
|
+
|
|
815
|
+
// wispy model (no args) — show current
|
|
816
|
+
if (!sub) {
|
|
817
|
+
const defaultP = config.defaultProvider ?? configuredProviders[0];
|
|
818
|
+
const currentModel = config.providers?.[defaultP]?.model ?? config.model ?? PROVIDERS[defaultP]?.defaultModel ?? "unknown";
|
|
819
|
+
console.log(`\n🤖 ${_bold("Current model")}: ${_cyan(defaultP)}:${_bold(currentModel)}\n`);
|
|
820
|
+
if (configuredProviders.length > 1) {
|
|
821
|
+
console.log(` Configured providers: ${configuredProviders.join(", ")}`);
|
|
822
|
+
console.log(` ${_dim("Use 'wispy model list' to see all options")}`);
|
|
823
|
+
console.log(` ${_dim("Use 'wispy model set <provider:model>' to switch")}\n`);
|
|
824
|
+
}
|
|
825
|
+
process.exit(0);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// wispy model list
|
|
829
|
+
if (sub === "list") {
|
|
830
|
+
console.log(`\n🤖 ${_bold("Available models")}\n`);
|
|
831
|
+
for (const p of configuredProviders) {
|
|
832
|
+
const currentModel = config.providers?.[p]?.model ?? PROVIDERS[p]?.defaultModel ?? "";
|
|
833
|
+
const models = KNOWN_MODELS[p] ?? [currentModel];
|
|
834
|
+
console.log(` ${_cyan(p)}:`);
|
|
835
|
+
for (const m of models) {
|
|
836
|
+
const isCurrent = m === currentModel;
|
|
837
|
+
console.log(` ${isCurrent ? _green("●") : " "} ${m}${isCurrent ? _dim(" (current)") : ""}`);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
console.log(`\n ${_dim("Switch with: wispy model set <provider:model>")}\n`);
|
|
841
|
+
process.exit(0);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// wispy model set <provider:model>
|
|
845
|
+
if (sub === "set") {
|
|
846
|
+
const spec = args[2];
|
|
847
|
+
if (!spec || !spec.includes(":")) {
|
|
848
|
+
console.error(_red(`\n❌ Usage: wispy model set <provider:model>\n Example: wispy model set xai:grok-3\n`));
|
|
849
|
+
process.exit(1);
|
|
850
|
+
}
|
|
851
|
+
const colonIdx = spec.indexOf(":");
|
|
852
|
+
const provName = spec.slice(0, colonIdx);
|
|
853
|
+
const modelName = spec.slice(colonIdx + 1);
|
|
854
|
+
|
|
855
|
+
if (!PROVIDERS[provName]) {
|
|
856
|
+
console.error(_red(`\n❌ Unknown provider: ${provName}\n Available: ${Object.keys(PROVIDERS).join(", ")}\n`));
|
|
857
|
+
process.exit(1);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// Update config
|
|
861
|
+
if (!config.providers) config.providers = {};
|
|
862
|
+
if (!config.providers[provName]) config.providers[provName] = {};
|
|
863
|
+
config.providers[provName].model = modelName;
|
|
864
|
+
if (!config.defaultProvider) config.defaultProvider = provName;
|
|
865
|
+
|
|
866
|
+
await saveConfig(config);
|
|
867
|
+
console.log(_green(`\n✅ Switched to ${_cyan(provName)}:${_bold(modelName)}\n`));
|
|
868
|
+
process.exit(0);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
console.error(_red(`\n❌ Unknown subcommand: wispy model ${sub}\n`));
|
|
872
|
+
console.log(_dim(" Usage: wispy model | wispy model list | wispy model set <provider:model>\n"));
|
|
873
|
+
process.exit(1);
|
|
874
|
+
}
|
|
875
|
+
|
|
763
876
|
// ── status sub-command ────────────────────────────────────────────────────────
|
|
764
877
|
if (args[0] === "status") {
|
|
765
878
|
// Try the enhanced status from onboarding.mjs first
|
|
@@ -1702,7 +1815,11 @@ if (_firstArg && _firstArg[0] !== "-" && !_KNOWN_COMMANDS.has(_firstArg)) {
|
|
|
1702
1815
|
// Not a known command — but could be a one-shot message (no quotes needed)
|
|
1703
1816
|
// Heuristic: if it looks like a real command word (no spaces, short), warn.
|
|
1704
1817
|
// If it looks like a natural language sentence, fall through to REPL one-shot mode.
|
|
1705
|
-
|
|
1818
|
+
// Only show "unknown command" if it looks very much like a mistyped subcommand.
|
|
1819
|
+
// Common typos/misspellings of real commands (2-char levenshtein distance).
|
|
1820
|
+
// Everything else falls through to one-shot message mode.
|
|
1821
|
+
const CLOSE_COMMANDS = ["seutp", "set", "stat", "statu", "doc", "wss", "trus", "depliy", "sycn", "cran"];
|
|
1822
|
+
const looksLikeCommand = CLOSE_COMMANDS.includes(_firstArg) || (_firstArg.length <= 3 && /^[a-z]+$/.test(_firstArg) && !["hi", "hey", "yo", "sup", "thx", "ty", "ok", "no", "yes", "ya", "hmm"].includes(_firstArg));
|
|
1706
1823
|
if (looksLikeCommand) {
|
|
1707
1824
|
// Show unknown command error
|
|
1708
1825
|
const suggestions = [
|
package/core/config.mjs
CHANGED
|
@@ -18,13 +18,45 @@ export const CONVERSATIONS_DIR = path.join(WISPY_DIR, "conversations");
|
|
|
18
18
|
export const MEMORY_DIR = path.join(WISPY_DIR, "memory");
|
|
19
19
|
|
|
20
20
|
export const PROVIDERS = {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
21
|
+
// ── Tier 0: Custom API providers ────────────────────────────────────────────
|
|
22
|
+
google: { envKeys: ["GOOGLE_AI_KEY", "GOOGLE_GENERATIVE_AI_KEY", "GEMINI_API_KEY"], defaultModel: "gemini-2.5-flash", label: "Google AI (Gemini)", signupUrl: "https://aistudio.google.com/apikey" },
|
|
23
|
+
anthropic: { envKeys: ["ANTHROPIC_API_KEY"], defaultModel: "claude-sonnet-4-20250514", label: "Anthropic (Claude)", signupUrl: "https://console.anthropic.com/settings/keys" },
|
|
24
|
+
|
|
25
|
+
// ── Tier 1: Popular OpenAI-compat ──────────────────────────────────────────
|
|
26
|
+
openai: { envKeys: ["OPENAI_API_KEY"], defaultModel: "gpt-4o", label: "OpenAI", signupUrl: "https://platform.openai.com/api-keys" },
|
|
27
|
+
xai: { envKeys: ["XAI_API_KEY"], defaultModel: "grok-3-mini", label: "xAI (Grok)", signupUrl: "https://console.x.ai/" },
|
|
28
|
+
mistral: { envKeys: ["MISTRAL_API_KEY"], defaultModel: "mistral-large-latest", label: "Mistral", signupUrl: "https://console.mistral.ai/api-keys/" },
|
|
29
|
+
together: { envKeys: ["TOGETHER_API_KEY"], defaultModel: "meta-llama/Llama-3.3-70B-Instruct-Turbo", label: "Together AI", signupUrl: "https://api.together.xyz/settings/api-keys" },
|
|
30
|
+
nvidia: { envKeys: ["NVIDIA_API_KEY"], defaultModel: "meta/llama-3.3-70b-instruct", label: "NVIDIA NIM", signupUrl: "https://build.nvidia.com/" },
|
|
31
|
+
|
|
32
|
+
// ── Tier 1: Fast/free ───────────────────────────────────────────────────────
|
|
33
|
+
groq: { envKeys: ["GROQ_API_KEY"], defaultModel: "llama-3.3-70b-versatile", label: "Groq (fast inference)", signupUrl: "https://console.groq.com/keys" },
|
|
34
|
+
deepseek: { envKeys: ["DEEPSEEK_API_KEY"], defaultModel: "deepseek-chat", label: "DeepSeek", signupUrl: "https://platform.deepseek.com/api_keys" },
|
|
35
|
+
chutes: { envKeys: ["CHUTES_API_KEY"], defaultModel: "deepseek-ai/DeepSeek-V3-0324", label: "Chutes", signupUrl: "https://chutes.ai/" },
|
|
36
|
+
|
|
37
|
+
// ── Tier 1: Aggregators ─────────────────────────────────────────────────────
|
|
38
|
+
openrouter: { envKeys: ["OPENROUTER_API_KEY"], defaultModel: "anthropic/claude-sonnet-4-20250514", label: "OpenRouter (multi-model)", signupUrl: "https://openrouter.ai/keys" },
|
|
39
|
+
vercelai: { envKeys: ["VERCEL_AI_TOKEN"], defaultModel: "anthropic/claude-3-5-sonnet", label: "Vercel AI Gateway", signupUrl: "https://vercel.com/docs/ai-gateway" },
|
|
40
|
+
|
|
41
|
+
// ── Tier 2: Useful ──────────────────────────────────────────────────────────
|
|
42
|
+
kimi: { envKeys: ["MOONSHOT_API_KEY", "KIMI_API_KEY"], defaultModel: "moonshot-v1-8k", label: "Kimi/Moonshot", signupUrl: "https://platform.moonshot.cn/console/api-keys" },
|
|
43
|
+
minimax: { envKeys: ["MINIMAX_API_KEY"], defaultModel: "MiniMax-Text-01", label: "MiniMax", signupUrl: "https://www.minimaxi.com/" },
|
|
44
|
+
venice: { envKeys: ["VENICE_API_KEY"], defaultModel: "llama-3.3-70b", label: "Venice AI", signupUrl: "https://venice.ai/chat/api" },
|
|
45
|
+
huggingface: { envKeys: ["HF_TOKEN", "HUGGINGFACE_API_KEY"], defaultModel: "meta-llama/Llama-3.3-70B-Instruct", label: "Hugging Face", signupUrl: "https://huggingface.co/settings/tokens", special: "hf_model_in_url" },
|
|
46
|
+
cloudflare: { envKeys: ["CF_API_TOKEN"], defaultModel: "@cf/meta/llama-3.3-70b-instruct-fp8-fast", label: "Cloudflare AI", signupUrl: "https://dash.cloudflare.com/", special: "cf_account_id" },
|
|
47
|
+
|
|
48
|
+
// ── Tier 3: Regional / niche ────────────────────────────────────────────────
|
|
49
|
+
volcengine: { envKeys: ["VOLCENGINE_API_KEY", "ARK_API_KEY"], defaultModel: "doubao-pro-32k", label: "Volcengine (ByteDance)", signupUrl: "https://console.volcengine.com/ark" },
|
|
50
|
+
byteplus: { envKeys: ["BYTEPLUS_API_KEY"], defaultModel: "doubao-pro-32k", label: "BytePlus", signupUrl: "https://console.byteplux.com/" },
|
|
51
|
+
zai: { envKeys: ["ZAI_API_KEY", "GLM_API_KEY", "ZHIPU_API_KEY"], defaultModel: "glm-4-flash", label: "Z.AI / GLM", signupUrl: "https://open.bigmodel.cn/" },
|
|
52
|
+
dashscope: { envKeys: ["DASHSCOPE_API_KEY"], defaultModel: "qwen-max", label: "DashScope (Alibaba)", signupUrl: "https://dashscope.aliyun.com/" },
|
|
53
|
+
xiaomi: { envKeys: ["XIAOMI_API_KEY"], defaultModel: "MiMo-7B-RL", label: "Xiaomi AI", signupUrl: "https://ai.xiaomi.com/" },
|
|
54
|
+
|
|
55
|
+
// ── Local / self-hosted ─────────────────────────────────────────────────────
|
|
56
|
+
ollama: { envKeys: [], defaultModel: "llama3.2", label: "Ollama (local)", signupUrl: null, local: true },
|
|
57
|
+
vllm: { envKeys: [], defaultModel: "meta-llama/Llama-3.3-70B-Instruct", label: "vLLM (self-hosted)", signupUrl: "https://docs.vllm.ai/", local: true },
|
|
58
|
+
sglang: { envKeys: [], defaultModel: "meta-llama/Llama-3.3-70B-Instruct", label: "SGLang (self-hosted)", signupUrl: "https://docs.sglang.ai/", local: true },
|
|
59
|
+
litellm: { envKeys: ["LITELLM_API_KEY"], defaultModel: "gpt-4o", label: "LiteLLM (proxy)", signupUrl: "https://docs.litellm.ai/", local: true },
|
|
28
60
|
};
|
|
29
61
|
|
|
30
62
|
async function tryKeychainKey(service) {
|
|
@@ -89,12 +121,32 @@ export async function detectProvider() {
|
|
|
89
121
|
}
|
|
90
122
|
}
|
|
91
123
|
|
|
92
|
-
// 3. Auto-detect from env vars
|
|
93
|
-
const order = [
|
|
124
|
+
// 3. Auto-detect from env vars (priority order)
|
|
125
|
+
const order = [
|
|
126
|
+
"google", "anthropic", "openai", "xai", "mistral",
|
|
127
|
+
"groq", "deepseek", "chutes", "together",
|
|
128
|
+
"openrouter", "vercelai",
|
|
129
|
+
"kimi", "minimax", "venice", "huggingface", "cloudflare",
|
|
130
|
+
"volcengine", "byteplus", "zai", "dashscope", "xiaomi", "nvidia",
|
|
131
|
+
"litellm",
|
|
132
|
+
"ollama", "vllm", "sglang",
|
|
133
|
+
];
|
|
94
134
|
for (const p of order) {
|
|
95
|
-
const
|
|
96
|
-
if (
|
|
97
|
-
|
|
135
|
+
const prov = PROVIDERS[p];
|
|
136
|
+
if (!prov) continue;
|
|
137
|
+
const key = getEnvKey(prov.envKeys);
|
|
138
|
+
if (key) {
|
|
139
|
+
return { provider: p, key, model: process.env.WISPY_MODEL ?? prov.defaultModel };
|
|
140
|
+
}
|
|
141
|
+
// Local providers: check URL env vars
|
|
142
|
+
if (p === "ollama" && process.env.OLLAMA_HOST) {
|
|
143
|
+
return { provider: p, key: null, model: process.env.WISPY_MODEL ?? prov.defaultModel };
|
|
144
|
+
}
|
|
145
|
+
if (p === "vllm" && process.env.VLLM_BASE_URL) {
|
|
146
|
+
return { provider: p, key: null, model: process.env.WISPY_MODEL ?? prov.defaultModel };
|
|
147
|
+
}
|
|
148
|
+
if (p === "sglang" && process.env.SGLANG_BASE_URL) {
|
|
149
|
+
return { provider: p, key: null, model: process.env.WISPY_MODEL ?? prov.defaultModel };
|
|
98
150
|
}
|
|
99
151
|
}
|
|
100
152
|
|
package/core/onboarding.mjs
CHANGED
|
@@ -27,7 +27,7 @@ import { mkdir, writeFile, readFile, appendFile } from "node:fs/promises";
|
|
|
27
27
|
import { existsSync, readFileSync } from "node:fs";
|
|
28
28
|
import { execSync } from "node:child_process";
|
|
29
29
|
|
|
30
|
-
import { confirm, checkbox, select, input, password } from "@inquirer/prompts";
|
|
30
|
+
import { confirm, checkbox, select, input, password, Separator } from "@inquirer/prompts";
|
|
31
31
|
|
|
32
32
|
import {
|
|
33
33
|
WISPY_DIR,
|
|
@@ -54,13 +54,41 @@ const red = (s) => `\x1b[31m${s}\x1b[0m`;
|
|
|
54
54
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
55
55
|
|
|
56
56
|
const PROVIDER_LIST = [
|
|
57
|
-
|
|
58
|
-
{ key: "
|
|
59
|
-
{ key: "
|
|
60
|
-
{ key: "
|
|
61
|
-
{ key: "
|
|
62
|
-
{ key: "
|
|
63
|
-
|
|
57
|
+
// ── Popular ─────────────────────────────────────────────────────────────────
|
|
58
|
+
{ key: "google", label: "🌟 Google AI (Gemini) — free tier", defaultModel: "gemini-2.5-flash", signupUrl: "https://aistudio.google.com/apikey" },
|
|
59
|
+
{ key: "anthropic", label: "🌟 Anthropic (Claude) — best quality", defaultModel: "claude-sonnet-4-20250514", signupUrl: "https://console.anthropic.com/settings/keys" },
|
|
60
|
+
{ key: "openai", label: "🌟 OpenAI (GPT-4o)", defaultModel: "gpt-4o", signupUrl: "https://platform.openai.com/api-keys" },
|
|
61
|
+
{ key: "xai", label: "🌟 xAI (Grok)", defaultModel: "grok-3-mini", signupUrl: "https://console.x.ai/" },
|
|
62
|
+
{ key: "mistral", label: "🌟 Mistral", defaultModel: "mistral-large-latest", signupUrl: "https://console.mistral.ai/api-keys/" },
|
|
63
|
+
// ── Free / Affordable ────────────────────────────────────────────────────────
|
|
64
|
+
{ key: "_sep1", label: "── Free / Affordable ──", separator: true },
|
|
65
|
+
{ key: "groq", label: "💰 Groq — fastest, free tier", defaultModel: "llama-3.3-70b-versatile", signupUrl: "https://console.groq.com/keys" },
|
|
66
|
+
{ key: "deepseek", label: "💰 DeepSeek — cheap & good", defaultModel: "deepseek-chat", signupUrl: "https://platform.deepseek.com/api_keys" },
|
|
67
|
+
{ key: "chutes", label: "💰 Chutes — affordable", defaultModel: "deepseek-ai/DeepSeek-V3-0324", signupUrl: "https://chutes.ai/" },
|
|
68
|
+
{ key: "together", label: "💰 Together — open models", defaultModel: "meta-llama/Llama-3.3-70B-Instruct-Turbo", signupUrl: "https://api.together.xyz/settings/api-keys" },
|
|
69
|
+
// ── Local / Self-hosted ──────────────────────────────────────────────────────
|
|
70
|
+
{ key: "_sep2", label: "── Local / Self-hosted ──", separator: true },
|
|
71
|
+
{ key: "ollama", label: "🏠 Ollama — local, no API key", defaultModel: "llama3.2", signupUrl: null },
|
|
72
|
+
{ key: "vllm", label: "🏠 vLLM — self-hosted", defaultModel: "meta-llama/Llama-3.3-70B-Instruct", signupUrl: "https://docs.vllm.ai/" },
|
|
73
|
+
{ key: "sglang", label: "🏠 SGLang — self-hosted", defaultModel: "meta-llama/Llama-3.3-70B-Instruct", signupUrl: "https://docs.sglang.ai/" },
|
|
74
|
+
{ key: "litellm", label: "🏠 LiteLLM — proxy any model", defaultModel: "gpt-4o", signupUrl: "https://docs.litellm.ai/" },
|
|
75
|
+
// ── Aggregators ──────────────────────────────────────────────────────────────
|
|
76
|
+
{ key: "_sep3", label: "── Aggregators ──", separator: true },
|
|
77
|
+
{ key: "openrouter", label: "🔀 OpenRouter — 200+ models", defaultModel: "anthropic/claude-sonnet-4-20250514", signupUrl: "https://openrouter.ai/keys" },
|
|
78
|
+
{ key: "vercelai", label: "🔀 Vercel AI Gateway", defaultModel: "anthropic/claude-3-5-sonnet", signupUrl: "https://vercel.com/docs/ai-gateway" },
|
|
79
|
+
// ── Regional / Enterprise ────────────────────────────────────────────────────
|
|
80
|
+
{ key: "_sep4", label: "── Regional / Enterprise ──", separator: true },
|
|
81
|
+
{ key: "kimi", label: "🌏 Kimi/Moonshot (China)", defaultModel: "moonshot-v1-8k", signupUrl: "https://platform.moonshot.cn/console/api-keys" },
|
|
82
|
+
{ key: "zai", label: "🌏 Z.AI/GLM (China)", defaultModel: "glm-4-flash", signupUrl: "https://open.bigmodel.cn/" },
|
|
83
|
+
{ key: "volcengine", label: "🌏 Volcengine/ByteDance", defaultModel: "doubao-pro-32k", signupUrl: "https://console.volcengine.com/ark" },
|
|
84
|
+
{ key: "dashscope", label: "🌏 DashScope/Alibaba", defaultModel: "qwen-max", signupUrl: "https://dashscope.aliyun.com/" },
|
|
85
|
+
{ key: "minimax", label: "🌏 MiniMax (China)", defaultModel: "MiniMax-Text-01", signupUrl: "https://www.minimaxi.com/" },
|
|
86
|
+
{ key: "byteplus", label: "🌏 BytePlus", defaultModel: "doubao-pro-32k", signupUrl: "https://console.byteplux.com/" },
|
|
87
|
+
{ key: "xiaomi", label: "🌏 Xiaomi AI", defaultModel: "MiMo-7B-RL", signupUrl: "https://ai.xiaomi.com/" },
|
|
88
|
+
{ key: "nvidia", label: "🏢 NVIDIA NIM", defaultModel: "meta/llama-3.3-70b-instruct", signupUrl: "https://build.nvidia.com/" },
|
|
89
|
+
{ key: "huggingface", label: "🏢 Hugging Face", defaultModel: "meta-llama/Llama-3.3-70B-Instruct", signupUrl: "https://huggingface.co/settings/tokens", special: "hf_model_in_url" },
|
|
90
|
+
{ key: "venice", label: "🏢 Venice AI", defaultModel: "llama-3.3-70b", signupUrl: "https://venice.ai/chat/api" },
|
|
91
|
+
{ key: "cloudflare", label: "🏢 Cloudflare AI", defaultModel: "@cf/meta/llama-3.3-70b-instruct-fp8-fast", signupUrl: "https://dash.cloudflare.com/", special: "cf_account_id" },
|
|
64
92
|
];
|
|
65
93
|
|
|
66
94
|
function getProviderLabel(key) {
|
|
@@ -130,6 +158,34 @@ async function validateApiKey(provider, key) {
|
|
|
130
158
|
});
|
|
131
159
|
return r.ok ? { ok: true, model: "anthropic/claude-sonnet-4-20250514" } : { ok: false, error: `HTTP ${r.status}` };
|
|
132
160
|
}
|
|
161
|
+
if (provider === "xai") {
|
|
162
|
+
const r = await fetch("https://api.x.ai/v1/models", {
|
|
163
|
+
headers: { Authorization: `Bearer ${key}` },
|
|
164
|
+
signal: AbortSignal.timeout(6000),
|
|
165
|
+
});
|
|
166
|
+
return r.ok ? { ok: true, model: "grok-3-mini" } : { ok: false, error: `HTTP ${r.status}` };
|
|
167
|
+
}
|
|
168
|
+
if (provider === "mistral") {
|
|
169
|
+
const r = await fetch("https://api.mistral.ai/v1/models", {
|
|
170
|
+
headers: { Authorization: `Bearer ${key}` },
|
|
171
|
+
signal: AbortSignal.timeout(6000),
|
|
172
|
+
});
|
|
173
|
+
return r.ok ? { ok: true, model: "mistral-large-latest" } : { ok: false, error: `HTTP ${r.status}` };
|
|
174
|
+
}
|
|
175
|
+
if (provider === "together") {
|
|
176
|
+
const r = await fetch("https://api.together.xyz/v1/models", {
|
|
177
|
+
headers: { Authorization: `Bearer ${key}` },
|
|
178
|
+
signal: AbortSignal.timeout(6000),
|
|
179
|
+
});
|
|
180
|
+
return r.ok ? { ok: true, model: "meta-llama/Llama-3.3-70B-Instruct-Turbo" } : { ok: false, error: `HTTP ${r.status}` };
|
|
181
|
+
}
|
|
182
|
+
if (provider === "huggingface") {
|
|
183
|
+
const r = await fetch("https://huggingface.co/api/whoami-v2", {
|
|
184
|
+
headers: { Authorization: `Bearer ${key}` },
|
|
185
|
+
signal: AbortSignal.timeout(6000),
|
|
186
|
+
});
|
|
187
|
+
return r.ok ? { ok: true, model: "meta-llama/Llama-3.3-70B-Instruct" } : { ok: false, error: `HTTP ${r.status}` };
|
|
188
|
+
}
|
|
133
189
|
// unknown provider — trust key format
|
|
134
190
|
return { ok: true, model: getProviderDefaultModel(provider) };
|
|
135
191
|
} catch (err) {
|
|
@@ -287,17 +343,23 @@ export class OnboardingWizard {
|
|
|
287
343
|
// ── Step 2: AI Providers (multi-select) ────────────────────────────────────
|
|
288
344
|
|
|
289
345
|
async stepProvider() {
|
|
290
|
-
console.log(`🤖 ${bold("AI Providers")}\n`);
|
|
346
|
+
console.log(`🤖 ${bold("AI Providers")} ${dim("(29 providers supported)")}\n`);
|
|
347
|
+
|
|
348
|
+
// Build checkbox choices with Separator support
|
|
349
|
+
const checkboxChoices = [];
|
|
350
|
+
for (const p of PROVIDER_LIST) {
|
|
351
|
+
if (p.separator) {
|
|
352
|
+
checkboxChoices.push(new Separator(p.label));
|
|
353
|
+
} else {
|
|
354
|
+
checkboxChoices.push({ name: p.label, value: p.key, checked: p.key === "google" });
|
|
355
|
+
}
|
|
356
|
+
}
|
|
291
357
|
|
|
292
358
|
let selectedKeys = [];
|
|
293
359
|
try {
|
|
294
360
|
selectedKeys = await checkbox({
|
|
295
361
|
message: "Which AI providers do you want to use? (space to select, enter to confirm)",
|
|
296
|
-
choices:
|
|
297
|
-
name: p.label,
|
|
298
|
-
value: p.key,
|
|
299
|
-
checked: i === 0, // Google pre-checked
|
|
300
|
-
})),
|
|
362
|
+
choices: checkboxChoices,
|
|
301
363
|
validate: (val) => val.length > 0 || "Select at least one provider (or press Ctrl+C to skip)",
|
|
302
364
|
});
|
|
303
365
|
} catch {
|
|
@@ -310,13 +372,17 @@ export class OnboardingWizard {
|
|
|
310
372
|
return {};
|
|
311
373
|
}
|
|
312
374
|
|
|
313
|
-
// Collect API keys for each selected provider
|
|
375
|
+
// Collect API keys / config for each selected provider
|
|
314
376
|
const providers = {};
|
|
315
377
|
for (const provKey of selectedKeys) {
|
|
378
|
+
const info = PROVIDER_LIST.find((p) => p.key === provKey);
|
|
379
|
+
|
|
380
|
+
// ── Local providers (no API key needed) ─────────────────────────────
|
|
316
381
|
if (provKey === "ollama") {
|
|
317
|
-
process.
|
|
382
|
+
const ollamaHost = process.env.OLLAMA_HOST ?? "http://localhost:11434";
|
|
383
|
+
process.stdout.write(`\n ${dim(`Checking Ollama at ${ollamaHost}...`)}`);
|
|
318
384
|
try {
|
|
319
|
-
const r = await fetch(
|
|
385
|
+
const r = await fetch(`${ollamaHost}/api/tags`, { signal: AbortSignal.timeout(3000) });
|
|
320
386
|
if (r.ok) {
|
|
321
387
|
process.stdout.write(green(" ✅\n"));
|
|
322
388
|
providers.ollama = { model: "llama3.2" };
|
|
@@ -328,7 +394,70 @@ export class OnboardingWizard {
|
|
|
328
394
|
continue;
|
|
329
395
|
}
|
|
330
396
|
|
|
331
|
-
|
|
397
|
+
if (provKey === "vllm" || provKey === "sglang") {
|
|
398
|
+
console.log("");
|
|
399
|
+
const envVar = provKey === "vllm" ? "VLLM_BASE_URL" : "SGLANG_BASE_URL";
|
|
400
|
+
const defaultUrl = provKey === "vllm" ? "http://localhost:8000" : "http://localhost:30000";
|
|
401
|
+
console.log(dim(` ${info?.label} — set ${envVar} env var, or enter URL now`));
|
|
402
|
+
try {
|
|
403
|
+
const baseUrl = await input({ message: ` Base URL:`, default: process.env[envVar] ?? defaultUrl });
|
|
404
|
+
providers[provKey] = { baseUrl, model: info?.defaultModel };
|
|
405
|
+
process.env[envVar] = baseUrl;
|
|
406
|
+
console.log(green(` ✅ ${info?.label} configured at ${baseUrl}`));
|
|
407
|
+
} catch { console.log(dim(` Skipping ${provKey}.`)); }
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (provKey === "litellm") {
|
|
412
|
+
console.log("");
|
|
413
|
+
console.log(dim(` LiteLLM proxy — configure any model (Bedrock, Vertex, etc.) via LiteLLM`));
|
|
414
|
+
console.log(dim(` Docs: https://docs.litellm.ai/`));
|
|
415
|
+
try {
|
|
416
|
+
const baseUrl = await input({ message: " LiteLLM Base URL:", default: process.env.LITELLM_BASE_URL ?? "http://localhost:4000" });
|
|
417
|
+
const apiKey = await password({ message: " LiteLLM API key (optional):", mask: "*" }).catch(() => "");
|
|
418
|
+
providers.litellm = { baseUrl, model: "gpt-4o", ...(apiKey ? { apiKey } : {}) };
|
|
419
|
+
process.env.LITELLM_BASE_URL = baseUrl;
|
|
420
|
+
if (apiKey) process.env.LITELLM_API_KEY = apiKey;
|
|
421
|
+
console.log(green(` ✅ LiteLLM configured at ${baseUrl}`));
|
|
422
|
+
} catch { console.log(dim(` Skipping litellm.`)); }
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// ── Special: Cloudflare needs account_id ─────────────────────────────
|
|
427
|
+
if (provKey === "cloudflare") {
|
|
428
|
+
console.log("");
|
|
429
|
+
console.log(dim(` Cloudflare AI — requires API token + Account ID`));
|
|
430
|
+
console.log(dim(` Get token at: https://dash.cloudflare.com/profile/api-tokens`));
|
|
431
|
+
try {
|
|
432
|
+
const cfToken = await password({ message: " CF API token:", mask: "*" });
|
|
433
|
+
const cfAccountId = await input({ message: " Account ID:", default: process.env.CF_ACCOUNT_ID ?? "" });
|
|
434
|
+
if (cfToken && cfAccountId) {
|
|
435
|
+
providers.cloudflare = { apiKey: cfToken, accountId: cfAccountId, model: info?.defaultModel };
|
|
436
|
+
process.env.CF_API_TOKEN = cfToken;
|
|
437
|
+
process.env.CF_ACCOUNT_ID = cfAccountId;
|
|
438
|
+
console.log(green(` ✅ Cloudflare AI configured`));
|
|
439
|
+
} else { console.log(dim(" Skipping cloudflare.")); }
|
|
440
|
+
} catch { console.log(dim(` Skipping cloudflare.`)); }
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// ── Special: HuggingFace needs model in URL ──────────────────────────
|
|
445
|
+
if (provKey === "huggingface") {
|
|
446
|
+
console.log("");
|
|
447
|
+
console.log(dim(` Hugging Face Inference API — get token at ${info?.signupUrl}`));
|
|
448
|
+
try {
|
|
449
|
+
const hfToken = await password({ message: " HF token (hf_...):", mask: "*" });
|
|
450
|
+
const hfModel = await input({ message: " Default model:", default: info?.defaultModel ?? "meta-llama/Llama-3.3-70B-Instruct" });
|
|
451
|
+
if (hfToken) {
|
|
452
|
+
providers.huggingface = { apiKey: hfToken, model: hfModel };
|
|
453
|
+
process.env.HF_TOKEN = hfToken;
|
|
454
|
+
console.log(green(` ✅ Hugging Face configured (${hfModel})`));
|
|
455
|
+
} else { console.log(dim(" Skipping huggingface.")); }
|
|
456
|
+
} catch { console.log(dim(` Skipping huggingface.`)); }
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// ── Standard API key flow ────────────────────────────────────────────
|
|
332
461
|
console.log("");
|
|
333
462
|
if (info?.signupUrl) {
|
|
334
463
|
console.log(dim(` Get a key at: ${info.signupUrl}`));
|
package/core/providers.mjs
CHANGED
|
@@ -14,11 +14,36 @@
|
|
|
14
14
|
import { PROVIDERS, detectProvider } from "./config.mjs";
|
|
15
15
|
|
|
16
16
|
const OPENAI_COMPAT_ENDPOINTS = {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
// ── Tier 1: Popular ───────────────────────────────────────────────────────
|
|
18
|
+
openai: "https://api.openai.com/v1/chat/completions",
|
|
19
|
+
xai: "https://api.x.ai/v1/chat/completions",
|
|
20
|
+
mistral: "https://api.mistral.ai/v1/chat/completions",
|
|
21
|
+
together: "https://api.together.xyz/v1/chat/completions",
|
|
22
|
+
nvidia: "https://integrate.api.nvidia.com/v1/chat/completions",
|
|
23
|
+
// ── Fast/free ─────────────────────────────────────────────────────────────
|
|
24
|
+
groq: "https://api.groq.com/openai/v1/chat/completions",
|
|
25
|
+
deepseek: "https://api.deepseek.com/v1/chat/completions",
|
|
26
|
+
chutes: "https://llm.chutes.ai/v1/chat/completions",
|
|
27
|
+
// ── Aggregators ───────────────────────────────────────────────────────────
|
|
28
|
+
openrouter: "https://openrouter.ai/api/v1/chat/completions",
|
|
29
|
+
vercelai: "https://gateway.ai.vercel.app/v1/chat/completions",
|
|
30
|
+
// ── Tier 2 ────────────────────────────────────────────────────────────────
|
|
31
|
+
kimi: "https://api.moonshot.cn/v1/chat/completions",
|
|
32
|
+
minimax: "https://api.minimax.chat/v1/text/chatcompletion_v2",
|
|
33
|
+
venice: "https://api.venice.ai/api/v1/chat/completions",
|
|
34
|
+
huggingface: null, // URL is model-specific: set dynamically
|
|
35
|
+
cloudflare: null, // URL is account-specific: set dynamically
|
|
36
|
+
// ── Tier 3: Regional ──────────────────────────────────────────────────────
|
|
37
|
+
volcengine: "https://ark.cn-beijing.volces.com/api/v3/chat/completions",
|
|
38
|
+
byteplus: "https://ark.bytepluse.com/api/v3/chat/completions",
|
|
39
|
+
zai: "https://open.bigmodel.cn/api/paas/v4/chat/completions",
|
|
40
|
+
dashscope: "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions",
|
|
41
|
+
xiaomi: "https://api.api2d.com/v1/chat/completions", // placeholder
|
|
42
|
+
// ── Local / self-hosted ───────────────────────────────────────────────────
|
|
43
|
+
ollama: null, // set from OLLAMA_HOST
|
|
44
|
+
vllm: null, // set from VLLM_BASE_URL
|
|
45
|
+
sglang: null, // set from SGLANG_BASE_URL
|
|
46
|
+
litellm: null, // set from LITELLM_BASE_URL
|
|
22
47
|
};
|
|
23
48
|
|
|
24
49
|
export class ProviderRegistry {
|
|
@@ -339,12 +364,31 @@ export class ProviderRegistry {
|
|
|
339
364
|
const inputText = openaiMessages.map(m => m.content ?? "").join("");
|
|
340
365
|
this._sessionTokens.input += this._estimateTokens(inputText);
|
|
341
366
|
|
|
342
|
-
|
|
367
|
+
// Resolve dynamic endpoints
|
|
368
|
+
let endpoint = OPENAI_COMPAT_ENDPOINTS[this._provider];
|
|
369
|
+
if (this._provider === "ollama") {
|
|
370
|
+
endpoint = `${process.env.OLLAMA_HOST ?? "http://localhost:11434"}/v1/chat/completions`;
|
|
371
|
+
} else if (this._provider === "vllm") {
|
|
372
|
+
endpoint = `${process.env.VLLM_BASE_URL ?? "http://localhost:8000"}/v1/chat/completions`;
|
|
373
|
+
} else if (this._provider === "sglang") {
|
|
374
|
+
endpoint = `${process.env.SGLANG_BASE_URL ?? "http://localhost:30000"}/v1/chat/completions`;
|
|
375
|
+
} else if (this._provider === "litellm") {
|
|
376
|
+
endpoint = `${process.env.LITELLM_BASE_URL ?? "http://localhost:4000"}/v1/chat/completions`;
|
|
377
|
+
} else if (this._provider === "huggingface") {
|
|
378
|
+
// HuggingFace: model is part of the URL
|
|
379
|
+
endpoint = `https://api-inference.huggingface.co/models/${model}/v1/chat/completions`;
|
|
380
|
+
} else if (this._provider === "cloudflare") {
|
|
381
|
+
const accountId = process.env.CF_ACCOUNT_ID ?? "";
|
|
382
|
+
endpoint = `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/v1/chat/completions`;
|
|
383
|
+
}
|
|
384
|
+
if (!endpoint) endpoint = OPENAI_COMPAT_ENDPOINTS.openai;
|
|
385
|
+
|
|
343
386
|
const headers = { "Content-Type": "application/json" };
|
|
344
387
|
if (this._apiKey) headers["Authorization"] = `Bearer ${this._apiKey}`;
|
|
345
388
|
if (this._provider === "openrouter") headers["HTTP-Referer"] = "https://wispy.dev";
|
|
389
|
+
if (this._provider === "cloudflare") headers["Authorization"] = `Bearer ${process.env.CF_API_TOKEN ?? this._apiKey}`;
|
|
346
390
|
|
|
347
|
-
const supportsTools = !["ollama"].includes(this._provider);
|
|
391
|
+
const supportsTools = !["ollama", "vllm", "sglang", "minimax", "huggingface"].includes(this._provider);
|
|
348
392
|
const body = { model, messages: openaiMessages, temperature: 0.7, max_tokens: 4096, stream: true };
|
|
349
393
|
if (supportsTools && tools.length > 0) {
|
|
350
394
|
body.tools = tools.map(t => ({
|