@parallel-cli/parallel 0.4.0 → 0.4.3

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/dist/config.js CHANGED
@@ -13,61 +13,250 @@ export function configFile() {
13
13
  }
14
14
  export const DEEPSEEK_PROVIDER = {
15
15
  name: 'DeepSeek',
16
- baseUrl: 'https://api.deepseek.com',
16
+ baseUrl: 'https://api.deepseek.com/v1',
17
17
  apiKey: '',
18
- models: ['deepseek-v4-flash', 'deepseek-v4-pro', 'deepseek-chat', 'deepseek-reasoner'],
19
- defaultModel: 'deepseek-v4-flash',
18
+ models: ['deepseek-v4-pro', 'deepseek-v4-flash', 'deepseek-chat', 'deepseek-reasoner'],
19
+ defaultModel: 'deepseek-v4-pro',
20
20
  };
21
21
  export const PROVIDER_PRESETS = [
22
+ // ── 🇺🇸 Western ──
22
23
  {
23
24
  name: 'OpenAI',
24
25
  baseUrl: 'https://api.openai.com/v1',
25
26
  apiKey: '',
26
- models: ['gpt-4o', 'gpt-4o-mini', 'o4-mini', 'gpt-4.1', 'gpt-4.1-mini'],
27
- defaultModel: 'gpt-4o',
27
+ models: ['gpt-5.5', 'gpt-5.5-pro', 'gpt-5.4', 'gpt-5.3-codex', 'gpt-4o', 'gpt-4o-mini', 'o4-mini', 'o3', 'o3-mini', 'o1', 'o1-mini'],
28
+ defaultModel: 'gpt-5.5',
29
+ category: 'western',
28
30
  },
29
- DEEPSEEK_PROVIDER,
30
31
  {
31
32
  name: 'Anthropic',
32
- baseUrl: 'https://api.anthropic.com/v1/',
33
+ baseUrl: 'https://api.anthropic.com/v1',
33
34
  apiKey: '',
34
- models: ['claude-sonnet-4-6', 'claude-opus-4-8'],
35
+ models: ['claude-opus-4-8', 'claude-opus-4-7', 'claude-sonnet-4-6', 'claude-haiku-4-5'],
35
36
  defaultModel: 'claude-sonnet-4-6',
37
+ category: 'western',
36
38
  },
37
39
  {
38
- name: 'OpenRouter',
39
- baseUrl: 'https://openrouter.ai/api/v1',
40
+ name: 'Google Gemini',
41
+ baseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai',
40
42
  apiKey: '',
41
- models: ['openai/gpt-4o', 'anthropic/claude-sonnet-4', 'deepseek/deepseek-chat', 'google/gemini-pro'],
42
- defaultModel: 'openai/gpt-4o',
43
+ models: ['gemini-3.1-pro', 'gemini-3.5-flash', 'gemini-3-flash', 'gemini-3.1-flash-lite'],
44
+ defaultModel: 'gemini-3.5-flash',
45
+ category: 'western',
43
46
  },
44
47
  {
45
- name: 'Gemini',
46
- baseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai/',
48
+ name: 'xAI Grok',
49
+ baseUrl: 'https://api.x.ai/v1',
47
50
  apiKey: '',
48
- models: ['gemini-3.5-flash', 'gemini-3.5-pro', 'gemini-2.5-pro'],
49
- defaultModel: 'gemini-3.5-flash',
51
+ models: ['grok-4', 'grok-4-fast-reasoning', 'grok-3', 'grok-code-fast-1'],
52
+ defaultModel: 'grok-4',
53
+ category: 'western',
50
54
  },
51
55
  {
52
56
  name: 'Mistral',
53
57
  baseUrl: 'https://api.mistral.ai/v1',
54
58
  apiKey: '',
55
- models: ['mistral-large-latest', 'mistral-small-latest', 'codestral-latest'],
56
- defaultModel: 'mistral-large-latest',
59
+ models: ['mistral-large-2', 'magistral-medium', 'codestral-latest', 'mistral-small-latest'],
60
+ defaultModel: 'mistral-large-2',
61
+ category: 'western',
62
+ },
63
+ {
64
+ name: 'Cohere',
65
+ baseUrl: 'https://api.cohere.com/v1',
66
+ apiKey: '',
67
+ models: ['command-a', 'command-r-plus'],
68
+ defaultModel: 'command-a',
69
+ category: 'western',
70
+ },
71
+ {
72
+ name: 'Perplexity',
73
+ baseUrl: 'https://api.perplexity.ai',
74
+ apiKey: '',
75
+ models: ['sonar-pro', 'sonar-deep-research'],
76
+ defaultModel: 'sonar-pro',
77
+ category: 'western',
78
+ },
79
+ // ── 🇨🇳 Chinese ──
80
+ {
81
+ name: 'DeepSeek',
82
+ baseUrl: 'https://api.deepseek.com/v1',
83
+ apiKey: '',
84
+ models: ['deepseek-v4-pro', 'deepseek-v4-flash', 'deepseek-chat', 'deepseek-reasoner'],
85
+ defaultModel: 'deepseek-v4-pro',
86
+ category: 'chinese',
87
+ },
88
+ {
89
+ name: 'MiniMax',
90
+ baseUrl: 'https://api.minimax.io/v1',
91
+ apiKey: '',
92
+ models: ['MiniMax-M3', 'MiniMax-M2.7', 'MiniMax-M2.7-highspeed'],
93
+ defaultModel: 'MiniMax-M3',
94
+ category: 'chinese',
95
+ },
96
+ {
97
+ name: 'Z.ai / GLM',
98
+ baseUrl: 'https://open.bigmodel.cn/api/paas/v4',
99
+ apiKey: '',
100
+ models: ['glm-5.2', 'glm-5.1', 'glm-4.7', 'glm-4.7-flash', 'glm-5v-turbo'],
101
+ defaultModel: 'glm-5.2',
102
+ category: 'chinese',
103
+ },
104
+ {
105
+ name: 'Alibaba / Qwen',
106
+ baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
107
+ apiKey: '',
108
+ models: ['qwen3.7-max', 'qwen3.6-max-preview', 'qwen3.6-plus', 'qwen3.5-coder'],
109
+ defaultModel: 'qwen3.7-max',
110
+ category: 'chinese',
111
+ },
112
+ {
113
+ name: 'Moonshot / Kimi',
114
+ baseUrl: 'https://api.moonshot.cn/v1',
115
+ apiKey: '',
116
+ models: ['kimi-k2.6', 'kimi-k2.7-code', 'kimi-k2.5', 'moonshot-v1-128k'],
117
+ defaultModel: 'kimi-k2.6',
118
+ category: 'chinese',
119
+ },
120
+ {
121
+ name: 'Xiaomi / MiMo',
122
+ baseUrl: 'https://api.minimaxi.com/v1',
123
+ apiKey: '',
124
+ models: ['mimo-v2-pro', 'mimo-v2-omni'],
125
+ defaultModel: 'mimo-v2-pro',
126
+ category: 'chinese',
127
+ },
128
+ {
129
+ name: 'StepFun',
130
+ baseUrl: 'https://api.stepfun.com/v1',
131
+ apiKey: '',
132
+ models: ['step-2-16k'],
133
+ defaultModel: 'step-2-16k',
134
+ category: 'chinese',
135
+ },
136
+ // ── 🌐 Gateways ──
137
+ {
138
+ name: 'OpenRouter',
139
+ baseUrl: 'https://openrouter.ai/api/v1',
140
+ apiKey: '',
141
+ models: ['openai/gpt-5.5', 'anthropic/claude-sonnet-4-6', 'google/gemini-3.5-flash', 'deepseek/deepseek-v4-pro', 'meta-llama/llama-4-maverick', 'mistralai/mistral-large-2'],
142
+ defaultModel: 'openai/gpt-5.5',
143
+ category: 'gateways',
57
144
  },
145
+ {
146
+ name: 'SiliconFlow',
147
+ baseUrl: 'https://api.siliconflow.cn/v1',
148
+ apiKey: '',
149
+ models: ['deepseek-ai/DeepSeek-V4-Pro', 'deepseek-ai/DeepSeek-R1', 'Qwen/Qwen3-Coder-480B', 'glm-4/GLM-5.2', 'moonshotai/Kimi-K2.6'],
150
+ defaultModel: 'deepseek-ai/DeepSeek-V4-Pro',
151
+ category: 'gateways',
152
+ },
153
+ {
154
+ name: 'Atlas Cloud',
155
+ baseUrl: 'https://api.atlascloud.ai/v1',
156
+ apiKey: '',
157
+ models: ['deepseek-v4-pro', 'deepseek-r1', 'qwen3.7-max', 'glm-5.2', 'kimi-k2.6', 'llama-4-maverick'],
158
+ defaultModel: 'deepseek-v4-pro',
159
+ category: 'gateways',
160
+ },
161
+ {
162
+ name: 'Requesty',
163
+ baseUrl: 'https://api.requesty.ai/api/v1',
164
+ apiKey: '',
165
+ models: ['gpt-5.5', 'claude-sonnet-4-6', 'gemini-3.5-flash', 'deepseek-v4-pro', 'llama-4-maverick', 'mistral-large-2'],
166
+ defaultModel: 'gpt-5.5',
167
+ category: 'gateways',
168
+ },
169
+ {
170
+ name: 'Vercel AI Gateway',
171
+ baseUrl: 'https://api.vercel.ai/v1',
172
+ apiKey: '',
173
+ models: ['gpt-5.5', 'claude-sonnet-4-6', 'gemini-3.5-flash', 'deepseek-v4-pro', 'llama-4-maverick'],
174
+ defaultModel: 'gpt-5.5',
175
+ category: 'gateways',
176
+ },
177
+ // ── ⚡ Inference ──
58
178
  {
59
179
  name: 'Groq',
60
180
  baseUrl: 'https://api.groq.com/openai/v1',
61
181
  apiKey: '',
62
- models: ['llama-3.3-70b-versatile', 'openai/gpt-oss-120b'],
63
- defaultModel: 'llama-3.3-70b-versatile',
182
+ models: ['qwen-2.5-coder-32b', 'deepseek-r1-distill-llama-70b', 'kimi-k2.6', 'llama-3.3-70b-versatile'],
183
+ defaultModel: 'qwen-2.5-coder-32b',
184
+ category: 'inference',
185
+ },
186
+ {
187
+ name: 'Cerebras',
188
+ baseUrl: 'https://api.cerebras.ai/v1',
189
+ apiKey: '',
190
+ models: ['llama-4-maverick-17b-128e-instruct', 'qwen3-coder-480b', 'kimi-k2.6', 'llama-3.3-70b'],
191
+ defaultModel: 'llama-4-maverick-17b-128e-instruct',
192
+ category: 'inference',
193
+ },
194
+ {
195
+ name: 'Together AI',
196
+ baseUrl: 'https://api.together.xyz/v1',
197
+ apiKey: '',
198
+ models: ['meta-llama/Llama-4-Maverick-17B-128E-Instruct', 'deepseek-ai/DeepSeek-V3', 'Qwen/Qwen3-Coder-480B', 'moonshotai/Kimi-K2.6'],
199
+ defaultModel: 'meta-llama/Llama-4-Maverick-17B-128E-Instruct',
200
+ category: 'inference',
201
+ },
202
+ {
203
+ name: 'Fireworks',
204
+ baseUrl: 'https://api.fireworks.ai/inference/v1',
205
+ apiKey: '',
206
+ models: ['accounts/fireworks/models/llama4-maverick-17b', 'accounts/fireworks/models/deepseek-v3', 'accounts/fireworks/models/qwen3-coder-480b', 'accounts/fireworks/models/kimi-k2.6'],
207
+ defaultModel: 'accounts/fireworks/models/llama4-maverick-17b',
208
+ category: 'inference',
209
+ },
210
+ {
211
+ name: 'DeepInfra',
212
+ baseUrl: 'https://api.deepinfra.com/v1/openai',
213
+ apiKey: '',
214
+ models: ['meta-llama/Llama-4-Maverick-17B-128E', 'deepseek-ai/DeepSeek-V3', 'Qwen/Qwen3-Coder-480B', 'moonshotai/Kimi-K2.6'],
215
+ defaultModel: 'meta-llama/Llama-4-Maverick-17B-128E',
216
+ category: 'inference',
64
217
  },
65
218
  {
66
- name: 'Together',
67
- baseUrl: 'https://api.together.ai/v1',
219
+ name: 'Novita',
220
+ baseUrl: 'https://api.novita.ai/v3/openai',
68
221
  apiKey: '',
69
- models: ['openai/gpt-oss-120b', 'openai/gpt-oss-20b'],
70
- defaultModel: 'openai/gpt-oss-120b',
222
+ models: ['meta-llama/llama-4-maverick-17b-128e', 'deepseek/deepseek-v3', 'qwen/qwen3-coder-480b', 'moonshotai/kimi-k2.6'],
223
+ defaultModel: 'meta-llama/llama-4-maverick-17b-128e',
224
+ category: 'inference',
225
+ },
226
+ {
227
+ name: 'Hyperbolic',
228
+ baseUrl: 'https://api.hyperbolic.ai/v1',
229
+ apiKey: '',
230
+ models: ['meta-llama/Llama-4-Maverick-17B-128E', 'deepseek-ai/DeepSeek-V3', 'Qwen/Qwen3-Coder-480B', 'moonshotai/Kimi-K2.6'],
231
+ defaultModel: 'meta-llama/Llama-4-Maverick-17B-128E',
232
+ category: 'inference',
233
+ },
234
+ {
235
+ name: 'SambaNova',
236
+ baseUrl: 'https://api.sambanova.ai/v1',
237
+ apiKey: '',
238
+ models: ['Meta-Llama-4-Maverick-17B-128E-Instruct', 'DeepSeek-V3', 'Llama-3.3-70B-Instruct'],
239
+ defaultModel: 'Meta-Llama-4-Maverick-17B-128E-Instruct',
240
+ category: 'inference',
241
+ },
242
+ // ── 🏠 Local ──
243
+ {
244
+ name: 'Ollama',
245
+ baseUrl: 'http://localhost:11434/v1',
246
+ apiKey: 'ollama-local',
247
+ models: ['qwen3-coder:480b', 'glm-4.7', 'deepseek-v3', 'kimi-k2', 'llama3.2', 'mistral', 'codellama', 'gemma3'],
248
+ defaultModel: 'qwen3-coder:480b',
249
+ category: 'local',
250
+ requiresApiKey: false,
251
+ },
252
+ {
253
+ name: 'vLLM / SGLang',
254
+ baseUrl: 'http://localhost:8000/v1',
255
+ apiKey: '',
256
+ models: ['your-model-here'],
257
+ defaultModel: '',
258
+ category: 'local',
259
+ requiresApiKey: false,
71
260
  },
72
261
  ];
73
262
  export const DEFAULTS = {
@@ -85,6 +274,15 @@ function normalizeApprovalMode(mode) {
85
274
  return 'auto-safe';
86
275
  return 'ask';
87
276
  }
277
+ export function isLocalProvider(p) {
278
+ return /localhost|127\.0\.0\.1|0\.0\.0\.0/.test(p.baseUrl);
279
+ }
280
+ export function providerNeedsApiKey(p) {
281
+ return p.requiresApiKey !== false && !isLocalProvider(p);
282
+ }
283
+ export function providerReady(p) {
284
+ return !providerNeedsApiKey(p) || p.apiKey.trim().length > 0;
285
+ }
88
286
  export function getProvider(cfg, name) {
89
287
  const n = (name ?? cfg.defaultProvider).toLowerCase();
90
288
  return cfg.providers.find((p) => p.name.toLowerCase() === n) ?? (name ? undefined : cfg.providers[0]);
@@ -115,6 +313,20 @@ function migrate(raw, cfg) {
115
313
  cfg.providers = [p];
116
314
  cfg.defaultProvider = p.name;
117
315
  }
316
+ function normalizeConfig(cfg) {
317
+ cfg.providers = cfg.providers.filter((p) => p && typeof p.name === 'string' && typeof p.baseUrl === 'string');
318
+ for (const p of cfg.providers) {
319
+ if (!Array.isArray(p.models))
320
+ p.models = [];
321
+ if (typeof p.defaultModel !== 'string')
322
+ p.defaultModel = p.models[0] ?? '';
323
+ if (typeof p.apiKey !== 'string')
324
+ p.apiKey = '';
325
+ }
326
+ const defaultExists = cfg.providers.some((p) => p.name.toLowerCase() === cfg.defaultProvider.toLowerCase());
327
+ if (!defaultExists)
328
+ cfg.defaultProvider = cfg.providers[0]?.name ?? '';
329
+ }
118
330
  export function loadConfig() {
119
331
  let cfg = { ...DEFAULTS, providers: [] };
120
332
  try {
@@ -131,17 +343,30 @@ export function loadConfig() {
131
343
  catch {
132
344
  // ignore corrupted config
133
345
  }
134
- // Env vars: ensure a DeepSeek provider exists / override the default provider.
135
- const envKey = process.env.PARALLEL_API_KEY || process.env.DEEPSEEK_API_KEY;
136
- if (envKey && cfg.providers.length === 0) {
137
- cfg.providers = [{ ...DEEPSEEK_PROVIDER, apiKey: envKey }];
346
+ normalizeConfig(cfg);
347
+ // Env vars: PARALLEL_API_KEY targets the current default provider; DEEPSEEK_API_KEY only targets DeepSeek.
348
+ const parallelKey = process.env.PARALLEL_API_KEY;
349
+ const deepseekKey = process.env.DEEPSEEK_API_KEY;
350
+ if (parallelKey && cfg.providers.length === 0) {
351
+ cfg.providers = [{ ...DEEPSEEK_PROVIDER, apiKey: parallelKey }];
138
352
  cfg.defaultProvider = DEEPSEEK_PROVIDER.name;
139
353
  }
140
- else if (envKey) {
354
+ else if (parallelKey) {
141
355
  const p = getProvider(cfg);
142
356
  if (p)
143
- p.apiKey = envKey;
357
+ p.apiKey = parallelKey;
358
+ }
359
+ if (deepseekKey) {
360
+ const p = getProvider(cfg, DEEPSEEK_PROVIDER.name);
361
+ if (p)
362
+ p.apiKey = deepseekKey;
363
+ else {
364
+ cfg.providers.push({ ...DEEPSEEK_PROVIDER, apiKey: deepseekKey });
365
+ if (!cfg.defaultProvider)
366
+ cfg.defaultProvider = DEEPSEEK_PROVIDER.name;
367
+ }
144
368
  }
369
+ normalizeConfig(cfg);
145
370
  const p = getProvider(cfg);
146
371
  if (p) {
147
372
  if (process.env.PARALLEL_BASE_URL)
@@ -112,17 +112,19 @@ export class Controller extends EventEmitter {
112
112
  }
113
113
  /** Resolve "model" or "provider:model" against the configured providers. */
114
114
  resolveModel(spec) {
115
- const m = spec.match(/^([^:]+):(.+)$/);
116
- if (m) {
117
- const provider = getProvider(this.config, m[1].trim());
118
- return provider ? { provider, model: m[2].trim() } : null;
115
+ const trimmed = spec.trim();
116
+ const sep = trimmed.indexOf(':');
117
+ if (sep > 0) {
118
+ const provider = getProvider(this.config, trimmed.slice(0, sep).trim());
119
+ if (provider)
120
+ return { provider, model: trimmed.slice(sep + 1).trim() };
119
121
  }
120
122
  // bare model name: current session provider first, then any provider listing it
121
123
  const cur = this.sessionProvider();
122
124
  if (cur)
123
- return { provider: cur, model: spec.trim() };
124
- const any = this.config.providers.find((p) => p.models.includes(spec.trim()));
125
- return any ? { provider: any, model: spec.trim() } : null;
125
+ return { provider: cur, model: trimmed };
126
+ const any = this.config.providers.find((p) => p.models.includes(trimmed));
127
+ return any ? { provider: any, model: trimmed } : null;
126
128
  }
127
129
  llmFor(provider, model) {
128
130
  const key = JSON.stringify([provider.name, provider.baseUrl, provider.apiKey, model]);
@@ -670,7 +672,7 @@ export class Controller extends EventEmitter {
670
672
  if (!p)
671
673
  return false;
672
674
  this.session.providerName = p.name;
673
- this.session.model = p.defaultModel;
675
+ this.session.model = p.defaultModel || p.models[0] || '';
674
676
  this.emit('update');
675
677
  return true;
676
678
  }
@@ -690,11 +692,11 @@ export class Controller extends EventEmitter {
690
692
  if (this.session.providerName.toLowerCase() === p.name.toLowerCase()) {
691
693
  this.session.providerName = p.name;
692
694
  if (!p.models.includes(this.session.model))
693
- this.session.model = p.defaultModel;
695
+ this.session.model = p.defaultModel || p.models[0] || '';
694
696
  }
695
697
  if (!this.session.providerName) {
696
698
  this.session.providerName = p.name;
697
- this.session.model = p.defaultModel;
699
+ this.session.model = p.defaultModel || p.models[0] || '';
698
700
  }
699
701
  this.emit('update');
700
702
  }
@@ -718,6 +720,26 @@ export class Controller extends EventEmitter {
718
720
  this.emit('update');
719
721
  return true;
720
722
  }
723
+ /** Remove a provider by name. Clears the default if it was the removed one. */
724
+ removeProvider(name) {
725
+ const idx = this.config.providers.findIndex(p => p.name.toLowerCase() === name.toLowerCase());
726
+ if (idx < 0)
727
+ return false;
728
+ this.config.providers.splice(idx, 1);
729
+ if (this.config.defaultProvider.toLowerCase() === name.toLowerCase()) {
730
+ this.config.defaultProvider = this.config.providers[0]?.name ?? '';
731
+ }
732
+ // If the session was using the removed provider, reset it
733
+ if (this.session.providerName.toLowerCase() === name.toLowerCase()) {
734
+ const fallback = this.config.providers[0];
735
+ this.session.providerName = fallback?.name ?? '';
736
+ this.session.model = fallback?.defaultModel || fallback?.models[0] || '';
737
+ }
738
+ saveConfig(this.config);
739
+ this.llmCache.clear();
740
+ this.emit('update');
741
+ return true;
742
+ }
721
743
  setGlobalApprovalMode(mode) {
722
744
  this.config.approvalMode = mode;
723
745
  saveConfig(this.config);