wispy-cli 2.4.3 → 2.5.0

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 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
@@ -499,24 +502,51 @@ if (args[0] === "doctor") {
499
502
 
500
503
  // 3. API key configured
501
504
  if (config) {
502
- const provider = config.provider ?? "unknown";
503
505
  const envMap = {
504
- google: "GOOGLE_AI_KEY",
505
- anthropic: "ANTHROPIC_API_KEY",
506
- openai: "OPENAI_API_KEY",
507
- groq: "GROQ_API_KEY",
508
- openrouter: "OPENROUTER_API_KEY",
509
- deepseek: "DEEPSEEK_API_KEY",
510
- ollama: null,
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
511
532
  };
512
- const envKey = envMap[provider];
513
- if (envKey === null) {
514
- check("AI provider (Ollama)", true, "no key needed");
515
- } else if (envKey) {
516
- const key = config.apiKey || process.env[envKey];
517
- check(`API key (${provider})`, !!key && key.length > 10, key ? "configured" : `run 'wispy setup provider'`);
533
+ // Support both old (config.provider) and new (config.providers) format
534
+ const providers = config.providers ? Object.keys(config.providers) : (config.provider ? [config.provider] : []);
535
+ if (providers.length === 0) {
536
+ check("AI provider", false, "no provider configured — run 'wispy setup provider'");
518
537
  } else {
519
- check("AI provider", false, `Unknown provider: ${provider}`);
538
+ for (const provider of providers) {
539
+ const envKeys = envMap[provider];
540
+ if (envKeys === null) {
541
+ check(`AI provider (${provider})`, true, "no key needed");
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'`);
546
+ } else {
547
+ check(`AI provider (${provider})`, false, `unknown provider`);
548
+ }
549
+ }
520
550
  }
521
551
  } else {
522
552
  info("AI provider", "skipped (no config)");
@@ -753,6 +783,96 @@ if (args[0] === "update") {
753
783
  process.exit(0);
754
784
  }
755
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
+
756
876
  // ── status sub-command ────────────────────────────────────────────────────────
757
877
  if (args[0] === "status") {
758
878
  // Try the enhanced status from onboarding.mjs first
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
- google: { envKeys: ["GOOGLE_AI_KEY", "GEMINI_API_KEY"], defaultModel: "gemini-2.5-flash", label: "Google AI (Gemini)", signupUrl: "https://aistudio.google.com/apikey" },
22
- anthropic: { envKeys: ["ANTHROPIC_API_KEY"], defaultModel: "claude-sonnet-4-20250514", label: "Anthropic (Claude)", signupUrl: "https://console.anthropic.com/settings/keys" },
23
- openai: { envKeys: ["OPENAI_API_KEY"], defaultModel: "gpt-4o", label: "OpenAI", signupUrl: "https://platform.openai.com/api-keys" },
24
- openrouter:{ envKeys: ["OPENROUTER_API_KEY"], defaultModel: "anthropic/claude-sonnet-4-20250514", label: "OpenRouter (multi-model)", signupUrl: "https://openrouter.ai/keys" },
25
- groq: { envKeys: ["GROQ_API_KEY"], defaultModel: "llama-3.3-70b-versatile", label: "Groq (fast inference)", signupUrl: "https://console.groq.com/keys" },
26
- deepseek: { envKeys: ["DEEPSEEK_API_KEY"], defaultModel: "deepseek-chat", label: "DeepSeek", signupUrl: "https://platform.deepseek.com/api_keys" },
27
- ollama: { envKeys: ["OLLAMA_HOST"], defaultModel: "llama3.2", label: "Ollama (local)", signupUrl: null, local: true },
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 = ["google", "anthropic", "openai", "openrouter", "groq", "deepseek", "ollama"];
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 key = getEnvKey(PROVIDERS[p].envKeys);
96
- if (key || (p === "ollama" && process.env.OLLAMA_HOST)) {
97
- return { provider: p, key, model: process.env.WISPY_MODEL ?? PROVIDERS[p].defaultModel };
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
 
@@ -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
- { key: "google", label: "Google AI (Gemini) — free tier ⭐", defaultModel: "gemini-2.5-flash", signupUrl: "https://aistudio.google.com/apikey" },
58
- { key: "anthropic", label: "Anthropic (Claude) — best quality", defaultModel: "claude-sonnet-4-20250514", signupUrl: "https://console.anthropic.com/settings/keys" },
59
- { key: "openai", label: "OpenAI (GPT-4o)", defaultModel: "gpt-4o", signupUrl: "https://platform.openai.com/api-keys" },
60
- { key: "groq", label: "Groq fastest, free", defaultModel: "llama-3.3-70b-versatile", signupUrl: "https://console.groq.com/keys" },
61
- { key: "deepseek", label: "DeepSeek cheap & good", defaultModel: "deepseek-chat", signupUrl: "https://platform.deepseek.com/api_keys" },
62
- { key: "ollama", label: "Ollama — local, no key needed", defaultModel: "llama3.2", signupUrl: null },
63
- { key: "openrouter", label: "OpenRouter — access any model", defaultModel: "anthropic/claude-sonnet-4-20250514", signupUrl: "https://openrouter.ai/keys" },
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: PROVIDER_LIST.map((p, i) => ({
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.stdout.write(`\n ${dim("Checking Ollama at http://localhost:11434...")}`);
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("http://localhost:11434/api/tags", { signal: AbortSignal.timeout(3000) });
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
- const info = PROVIDER_LIST.find((p) => p.key === provKey);
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}`));
@@ -14,11 +14,36 @@
14
14
  import { PROVIDERS, detectProvider } from "./config.mjs";
15
15
 
16
16
  const OPENAI_COMPAT_ENDPOINTS = {
17
- openai: "https://api.openai.com/v1/chat/completions",
18
- openrouter: "https://openrouter.ai/api/v1/chat/completions",
19
- groq: "https://api.groq.com/openai/v1/chat/completions",
20
- deepseek: "https://api.deepseek.com/v1/chat/completions",
21
- ollama: `${process.env.OLLAMA_HOST ?? "http://localhost:11434"}/v1/chat/completions`,
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
- const endpoint = OPENAI_COMPAT_ENDPOINTS[this._provider] ?? OPENAI_COMPAT_ENDPOINTS.openai;
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 => ({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wispy-cli",
3
- "version": "2.4.3",
3
+ "version": "2.5.0",
4
4
  "description": "🌿 Wispy — AI workspace assistant with trustworthy execution (harness, receipts, approvals, diffs)",
5
5
  "license": "MIT",
6
6
  "author": "Minseo & Poropo",