lazyclaw 6.1.0 → 6.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lazyclaw",
3
- "version": "6.1.0",
3
+ "version": "6.2.0",
4
4
  "description": "Lazy, elegant terminal CLI for chatting with Claude / OpenAI / Gemini / Ollama, orchestrating multi-step LLM workflows, and running multi-agent Slack teams with cross-task memory. Banner-on-launch, slash-command ghost autocomplete, persistent sessions, local HTTP gateway.",
5
5
  "keywords": [
6
6
  "claude",
@@ -144,6 +144,16 @@ export const codexCliProvider = {
144
144
  const onAbort = () => { try { proc.kill('SIGTERM'); } catch (_) { /* ignore */ } };
145
145
  if (opts.signal) opts.signal.addEventListener('abort', onAbort);
146
146
 
147
+ // A missing binary surfaces as an ASYNC ChildProcess 'error' event on
148
+ // some platforms (the sync try/catch above doesn't see it). Without a
149
+ // listener that's an uncaughtException that kills the WHOLE process mid
150
+ // `providers test` — capture it and surface a per-provider CliMissingError
151
+ // instead (same fix claude_cli.mjs received in F8).
152
+ let spawnError = null;
153
+ const spawnErrorPromise = new Promise((resolve) => {
154
+ proc.once('error', (err) => { spawnError = err; resolve(); });
155
+ });
156
+
147
157
  let stderr = '';
148
158
  proc.stderr.setEncoding('utf8');
149
159
  proc.stderr.on('data', (chunk) => { stderr += chunk; });
@@ -184,7 +194,13 @@ export const codexCliProvider = {
184
194
  if (text) yield text;
185
195
  } catch (_) { /* incomplete tail — drop */ }
186
196
  }
187
- await exitPromise;
197
+ // Wait for either a clean exit or an async spawn error. On ENOENT the
198
+ // process never starts, so 'close' never fires — racing against
199
+ // spawnErrorPromise keeps this from hanging forever.
200
+ await Promise.race([exitPromise, spawnErrorPromise]);
201
+ if (spawnError) {
202
+ throw spawnError.code === 'ENOENT' ? new CliMissingError() : spawnError;
203
+ }
188
204
  if (exitInfo && exitInfo.code !== 0 && !opts.signal?.aborted) {
189
205
  throw new CliExitError(exitInfo.code, exitInfo.signal, stderr);
190
206
  }
@@ -0,0 +1,161 @@
1
+ // providers/compat_vendors.mjs — the built-in OpenAI-compatible vendor
2
+ // catalogue, split out of registry.mjs (file-size ratchet). Pure data:
3
+ // registry.mjs imports this and expands it into PROVIDERS / PROVIDER_INFO.
4
+
5
+ // Built-in OpenAI-compatible vendors. Same wire format → one factory call
6
+ // each. The picker treats these like first-class providers so users don't
7
+ // have to walk through "+ Add a custom endpoint" for the popular ones.
8
+ //
9
+ // Each entry must define baseUrl + envKey (the env var the chat path
10
+ // consults when no api-key is configured) + suggestedModels (curated list
11
+ // shown before the user fetches the live /v1/models catalogue).
12
+ //
13
+ // Adding a new vendor: drop a row here. The PROVIDERS / PROVIDER_INFO loops
14
+ // below pick it up automatically.
15
+ export const OPENAI_COMPAT_BUILTINS = {
16
+ nim: {
17
+ label: 'NVIDIA NIM',
18
+ baseUrl: 'https://integrate.api.nvidia.com/v1',
19
+ envKey: 'NVIDIA_API_KEY',
20
+ altEnvKeys: ['NIM_API_KEY'],
21
+ keyPrefix: 'nvapi-',
22
+ docs: 'NVIDIA NIM hosted catalogue (Llama 3.x, Nemotron, DeepSeek-R1, Mixtral, Phi-3, Qwen, etc.). Auth: NVIDIA_API_KEY env var or in-app api-key. Endpoint speaks the OpenAI v1 wire format.',
23
+ defaultModel: 'meta/llama-3.1-405b-instruct',
24
+ suggestedModels: [
25
+ 'meta/llama-3.1-405b-instruct',
26
+ 'meta/llama-3.1-70b-instruct',
27
+ 'meta/llama-3.1-8b-instruct',
28
+ 'nvidia/llama-3.1-nemotron-70b-instruct',
29
+ 'nvidia/nemotron-mini-4b-instruct',
30
+ 'nvidia/llama-3.3-nemotron-super-49b-v1',
31
+ 'mistralai/mistral-nemo-12b-instruct',
32
+ 'mistralai/mixtral-8x22b-instruct-v0.1',
33
+ 'microsoft/phi-3-medium-4k-instruct',
34
+ 'deepseek-ai/deepseek-r1',
35
+ 'qwen/qwen2.5-7b-instruct',
36
+ 'qwen/qwen2.5-coder-32b-instruct',
37
+ ],
38
+ },
39
+ openrouter: {
40
+ label: 'OpenRouter',
41
+ baseUrl: 'https://openrouter.ai/api/v1',
42
+ envKey: 'OPENROUTER_API_KEY',
43
+ keyPrefix: 'sk-or-',
44
+ docs: 'OpenRouter unified gateway — 200+ models behind one OpenAI-compatible endpoint. Auth: OPENROUTER_API_KEY env var or in-app api-key. Uses x-title/HTTP-Referer headers for attribution.',
45
+ defaultModel: 'anthropic/claude-3.5-sonnet',
46
+ headers: { 'http-referer': 'https://github.com/cmblir/lazyclaude', 'x-title': 'lazyclaw' },
47
+ suggestedModels: [
48
+ 'anthropic/claude-3.5-sonnet',
49
+ 'anthropic/claude-3-opus',
50
+ 'openai/gpt-4o',
51
+ 'openai/gpt-4o-mini',
52
+ 'openai/o1-preview',
53
+ 'meta-llama/llama-3.1-405b-instruct',
54
+ 'meta-llama/llama-3.3-70b-instruct',
55
+ 'google/gemini-2.0-flash-exp:free',
56
+ 'google/gemini-pro-1.5',
57
+ 'deepseek/deepseek-chat',
58
+ 'deepseek/deepseek-r1',
59
+ 'qwen/qwen-2.5-coder-32b-instruct',
60
+ 'mistralai/mistral-large',
61
+ ],
62
+ },
63
+ groq: {
64
+ label: 'Groq',
65
+ baseUrl: 'https://api.groq.com/openai/v1',
66
+ envKey: 'GROQ_API_KEY',
67
+ keyPrefix: 'gsk_',
68
+ docs: 'Groq LPU inference — fastest-token-per-second tier for Llama / Mixtral / Gemma. Auth: GROQ_API_KEY env var or in-app api-key.',
69
+ defaultModel: 'llama-3.3-70b-versatile',
70
+ suggestedModels: [
71
+ 'llama-3.3-70b-versatile',
72
+ 'llama-3.1-70b-versatile',
73
+ 'llama-3.1-8b-instant',
74
+ 'llama-3.2-90b-vision-preview',
75
+ 'mixtral-8x7b-32768',
76
+ 'gemma2-9b-it',
77
+ 'qwen-2.5-coder-32b',
78
+ 'qwen-2.5-32b',
79
+ 'deepseek-r1-distill-llama-70b',
80
+ ],
81
+ },
82
+ together: {
83
+ label: 'Together AI',
84
+ baseUrl: 'https://api.together.xyz/v1',
85
+ envKey: 'TOGETHER_API_KEY',
86
+ docs: 'Together AI hosted inference for open-weight models (Llama, Mixtral, Qwen, DeepSeek, etc.). Auth: TOGETHER_API_KEY env var or in-app api-key.',
87
+ defaultModel: 'meta-llama/Llama-3.3-70B-Instruct-Turbo',
88
+ suggestedModels: [
89
+ 'meta-llama/Llama-3.3-70B-Instruct-Turbo',
90
+ 'meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo',
91
+ 'meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo',
92
+ 'meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo',
93
+ 'mistralai/Mixtral-8x22B-Instruct-v0.1',
94
+ 'mistralai/Mixtral-8x7B-Instruct-v0.1',
95
+ 'Qwen/Qwen2.5-72B-Instruct-Turbo',
96
+ 'Qwen/Qwen2.5-Coder-32B-Instruct',
97
+ 'deepseek-ai/DeepSeek-V3',
98
+ 'deepseek-ai/DeepSeek-R1',
99
+ ],
100
+ },
101
+ xai: {
102
+ label: 'xAI (Grok)',
103
+ baseUrl: 'https://api.x.ai/v1',
104
+ envKey: 'XAI_API_KEY',
105
+ altEnvKeys: ['GROK_API_KEY'],
106
+ keyPrefix: 'xai-',
107
+ docs: 'xAI Grok models. Auth: XAI_API_KEY env var or in-app api-key.',
108
+ defaultModel: 'grok-2-latest',
109
+ suggestedModels: [
110
+ 'grok-2-latest',
111
+ 'grok-2-1212',
112
+ 'grok-2-vision-1212',
113
+ 'grok-beta',
114
+ 'grok-vision-beta',
115
+ ],
116
+ },
117
+ deepseek: {
118
+ label: 'DeepSeek',
119
+ baseUrl: 'https://api.deepseek.com/v1',
120
+ envKey: 'DEEPSEEK_API_KEY',
121
+ keyPrefix: 'sk-',
122
+ docs: 'DeepSeek (deepseek-chat / deepseek-reasoner). Auth: DEEPSEEK_API_KEY env var or in-app api-key.',
123
+ defaultModel: 'deepseek-chat',
124
+ suggestedModels: [
125
+ 'deepseek-chat',
126
+ 'deepseek-reasoner',
127
+ 'deepseek-coder',
128
+ ],
129
+ },
130
+ mistral: {
131
+ label: 'Mistral La Plateforme',
132
+ baseUrl: 'https://api.mistral.ai/v1',
133
+ envKey: 'MISTRAL_API_KEY',
134
+ docs: 'Mistral La Plateforme (mistral-large, codestral, ministral, pixtral). Auth: MISTRAL_API_KEY env var or in-app api-key.',
135
+ defaultModel: 'mistral-large-latest',
136
+ suggestedModels: [
137
+ 'mistral-large-latest',
138
+ 'mistral-small-latest',
139
+ 'codestral-latest',
140
+ 'ministral-8b-latest',
141
+ 'ministral-3b-latest',
142
+ 'pixtral-large-latest',
143
+ 'open-mistral-nemo',
144
+ ],
145
+ },
146
+ fireworks: {
147
+ label: 'Fireworks AI',
148
+ baseUrl: 'https://api.fireworks.ai/inference/v1',
149
+ envKey: 'FIREWORKS_API_KEY',
150
+ docs: 'Fireworks AI hosted models. Auth: FIREWORKS_API_KEY env var or in-app api-key.',
151
+ defaultModel: 'accounts/fireworks/models/llama-v3p3-70b-instruct',
152
+ suggestedModels: [
153
+ 'accounts/fireworks/models/llama-v3p3-70b-instruct',
154
+ 'accounts/fireworks/models/llama-v3p1-405b-instruct',
155
+ 'accounts/fireworks/models/qwen2p5-coder-32b-instruct',
156
+ 'accounts/fireworks/models/deepseek-r1',
157
+ 'accounts/fireworks/models/deepseek-v3',
158
+ 'accounts/fireworks/models/mixtral-8x22b-instruct',
159
+ ],
160
+ },
161
+ };
@@ -133,6 +133,16 @@ export const geminiCliProvider = {
133
133
  const onAbort = () => { try { proc.kill('SIGTERM'); } catch (_) { /* ignore */ } };
134
134
  if (opts.signal) opts.signal.addEventListener('abort', onAbort);
135
135
 
136
+ // A missing binary surfaces as an ASYNC ChildProcess 'error' event on
137
+ // some platforms (the sync try/catch above doesn't see it). Without a
138
+ // listener that's an uncaughtException that kills the WHOLE process mid
139
+ // `providers test` — capture it and surface a per-provider CliMissingError
140
+ // instead (same fix claude_cli.mjs received in F8).
141
+ let spawnError = null;
142
+ const spawnErrorPromise = new Promise((resolve) => {
143
+ proc.once('error', (err) => { spawnError = err; resolve(); });
144
+ });
145
+
136
146
  let stdout = '';
137
147
  let stderr = '';
138
148
  proc.stdout.setEncoding('utf8');
@@ -140,9 +150,15 @@ export const geminiCliProvider = {
140
150
  proc.stdout.on('data', (c) => { stdout += c; });
141
151
  proc.stderr.on('data', (c) => { stderr += c; });
142
152
 
143
- const exitInfo = await new Promise((resolve) => {
144
- proc.on('close', (code, signal) => resolve({ code, signal }));
145
- });
153
+ const exitInfo = await Promise.race([
154
+ new Promise((resolve) => { proc.on('close', (code, signal) => resolve({ code, signal })); }),
155
+ spawnErrorPromise.then(() => null),
156
+ ]);
157
+ if (spawnError) {
158
+ if (opts.signal) opts.signal.removeEventListener('abort', onAbort);
159
+ if (spawnError.code === 'ENOENT') throw new CliMissingError();
160
+ throw spawnError;
161
+ }
146
162
 
147
163
  try {
148
164
  if (opts.signal?.aborted) throw new AbortError('aborted mid-run');
@@ -11,13 +11,18 @@
11
11
  // Dependency-injected (no cli.mjs internals) so it stays import-light and
12
12
  // unit-testable with no network.
13
13
 
14
+ import fs from 'node:fs';
15
+ import os from 'node:os';
16
+ import path from 'node:path';
17
+
14
18
  /**
15
- * Whether a provider exposes an OpenAI-compatible `/v1/models` catalogue we
16
- * can live-fetch. True for openai, ollama, any builtin OpenAI-compat vendor
17
- * (nim / openrouter / groq / together / xai / deepseek / mistral /
18
- * fireworks), and any provider carrying an explicit `baseUrl` (custom
19
- * endpoints). False for anthropic / gemini / claude-cli / mock /
20
- * orchestrator.
19
+ * Whether a provider exposes a model catalogue we can live-fetch. True for
20
+ * openai, ollama, any builtin OpenAI-compat vendor (nim / openrouter / groq /
21
+ * together / xai / deepseek / mistral / fireworks), any provider carrying an
22
+ * explicit `baseUrl` (custom endpoints), and via their NATIVE list
23
+ * endpoints anthropic (`GET /v1/models`) and gemini
24
+ * (`GET /v1beta/models`). False for claude-cli (keyless subprocess, no
25
+ * catalogue endpoint) / mock / orchestrator.
21
26
  *
22
27
  * @param {object} meta PROVIDER_INFO[providerId]
23
28
  * @param {string} providerId
@@ -28,9 +33,70 @@ export function supportsLiveFetch(meta, providerId) {
28
33
  return !!m.baseUrl
29
34
  || providerId === 'openai'
30
35
  || providerId === 'ollama'
36
+ || providerId === 'anthropic'
37
+ || providerId === 'gemini'
38
+ // Keyless CLI providers borrow the credential their vendor accepts
39
+ // (anthropic key / Claude Code OAuth token; gemini key; openai key or
40
+ // a plain key stored in ~/.codex/auth.json) — best-effort with an
41
+ // honest, actionable error when none is available.
42
+ || providerId === 'claude-cli'
43
+ || providerId === 'gemini-cli'
44
+ || providerId === 'codex-cli'
31
45
  || !!m.builtinOpenAICompat;
32
46
  }
33
47
 
48
+ /**
49
+ * Live-list Anthropic models via the native Models API. Surfaces newly
50
+ * released models (e.g. claude-fable-5) the day they ship instead of waiting
51
+ * for a curated-list update. Sorted, deduped.
52
+ *
53
+ * @param {{apiKey:string, fetchImpl?:typeof fetch}} opts
54
+ * @returns {Promise<string[]>}
55
+ */
56
+ export async function fetchAnthropicModels({ apiKey, oauthToken, fetchImpl } = {}) {
57
+ if (!apiKey && !oauthToken) throw new Error('anthropic model listing requires an api key (set ANTHROPIC_API_KEY or configure the provider)');
58
+ const f = fetchImpl || globalThis.fetch;
59
+ // OAuth tokens (Claude Code subscription login) authenticate with a Bearer
60
+ // header plus the documented oauth beta header; api keys use x-api-key.
61
+ const auth = apiKey
62
+ ? { 'x-api-key': apiKey }
63
+ : { 'authorization': `Bearer ${oauthToken}`, 'anthropic-beta': 'oauth-2025-04-20' };
64
+ const res = await f('https://api.anthropic.com/v1/models?limit=1000', {
65
+ method: 'GET',
66
+ headers: { ...auth, 'anthropic-version': '2023-06-01', 'accept': 'application/json' },
67
+ });
68
+ if (!res.ok) throw new Error(`anthropic /v1/models returned HTTP ${res.status}`);
69
+ const obj = await res.json();
70
+ const ids = (Array.isArray(obj?.data) ? obj.data : [])
71
+ .map((m) => m && m.id)
72
+ .filter((id) => typeof id === 'string');
73
+ return Array.from(new Set(ids)).sort((a, b) => a.localeCompare(b));
74
+ }
75
+
76
+ /**
77
+ * Live-list Gemini models via the Generative Language API, keeping only
78
+ * chat-capable entries (supportedGenerationMethods includes
79
+ * generateContent) and stripping the `models/` resource prefix.
80
+ *
81
+ * @param {{apiKey:string, fetchImpl?:typeof fetch}} opts
82
+ * @returns {Promise<string[]>}
83
+ */
84
+ export async function fetchGeminiModels({ apiKey, fetchImpl } = {}) {
85
+ if (!apiKey) throw new Error('gemini model listing requires an api key (set GEMINI_API_KEY or configure the provider)');
86
+ const f = fetchImpl || globalThis.fetch;
87
+ const res = await f(`https://generativelanguage.googleapis.com/v1beta/models?pageSize=1000&key=${encodeURIComponent(apiKey)}`, {
88
+ method: 'GET',
89
+ headers: { 'accept': 'application/json' },
90
+ });
91
+ if (!res.ok) throw new Error(`gemini models list returned HTTP ${res.status}`);
92
+ const obj = await res.json();
93
+ const ids = (Array.isArray(obj?.models) ? obj.models : [])
94
+ .filter((m) => Array.isArray(m?.supportedGenerationMethods) && m.supportedGenerationMethods.includes('generateContent'))
95
+ .map((m) => String(m.name || '').replace(/^models\//, ''))
96
+ .filter(Boolean);
97
+ return Array.from(new Set(ids)).sort((a, b) => a.localeCompare(b));
98
+ }
99
+
34
100
  /**
35
101
  * Resolve `{ baseUrl, apiKey }` for a provider's OpenAI-compatible
36
102
  * `/v1/models` endpoint. Returns `null` when the provider has no such
@@ -75,12 +141,79 @@ export function modelCatalogueFor({ cfg, registryMod, resolveAuthKey, providerId
75
141
  * @param {object} deps same shape as {@link modelCatalogueFor}
76
142
  * @returns {Promise<string[]>}
77
143
  */
144
+ // Claude Code OAuth token from the credential store `claude login` writes on
145
+ // Linux / non-keychain setups. On macOS the token lives in the OS Keychain
146
+ // (no file), so this returns null there — the caller falls through to its
147
+ // honest "set ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN" error. Read-only;
148
+ // the token is only ever sent to api.anthropic.com, never logged.
149
+ export function _claudeCodeOAuthToken({ home, readFileSync } = {}) {
150
+ const h = home || os.homedir();
151
+ const read = readFileSync || fs.readFileSync;
152
+ for (const rel of ['.claude/.credentials.json', '.config/claude/.credentials.json']) {
153
+ try {
154
+ const j = JSON.parse(read(path.join(h, rel), 'utf8'));
155
+ const tok = j?.claudeAiOauth?.accessToken || j?.accessToken || j?.access_token;
156
+ if (typeof tok === 'string' && tok) return tok;
157
+ } catch { /* missing / unreadable / not JSON — try the next location */ }
158
+ }
159
+ return null;
160
+ }
161
+
162
+ // A plain API key stored by `codex login --api-key` in ~/.codex/auth.json.
163
+ // ChatGPT-OAuth logins store an empty OPENAI_API_KEY object plus OAuth
164
+ // tokens — those do NOT authenticate the platform /v1/models endpoint, so
165
+ // they are deliberately ignored.
166
+ export function _codexStoredApiKey({ home, readFileSync } = {}) {
167
+ const h = home || os.homedir();
168
+ const read = readFileSync || fs.readFileSync;
169
+ try {
170
+ const j = JSON.parse(read(path.join(h, '.codex/auth.json'), 'utf8'));
171
+ const k = j?.OPENAI_API_KEY;
172
+ if (typeof k === 'string' && k) return k;
173
+ if (k && typeof k === 'object' && typeof k.value === 'string' && k.value) return k.value;
174
+ } catch { /* missing / unreadable */ }
175
+ return null;
176
+ }
177
+
78
178
  export async function fetchModelsForProvider(deps) {
79
- const c = modelCatalogueFor(deps);
80
179
  const providerId = deps && deps.providerId;
180
+ const key = (id) => (typeof deps?.resolveAuthKey === 'function' ? deps.resolveAuthKey(id) : '') || '';
181
+ const credReader = deps?._credReader; // test seam: () => token|null per helper
182
+ // Native-API providers list through their own endpoints (they are not
183
+ // OpenAI-compatible). Env fallbacks cover the common keyless-config case.
184
+ if (providerId === 'anthropic') {
185
+ return fetchAnthropicModels({ apiKey: key('anthropic') || process.env.ANTHROPIC_API_KEY || '', fetchImpl: deps?.fetchImpl });
186
+ }
187
+ if (providerId === 'gemini') {
188
+ return fetchGeminiModels({ apiKey: key('gemini') || process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY || '', fetchImpl: deps?.fetchImpl });
189
+ }
190
+ // Keyless CLI providers: borrow the credential their vendor accepts.
191
+ if (providerId === 'claude-cli') {
192
+ const apiKey = key('claude-cli') || key('anthropic') || process.env.ANTHROPIC_API_KEY || '';
193
+ if (apiKey) return fetchAnthropicModels({ apiKey, fetchImpl: deps?.fetchImpl });
194
+ const oauthToken = process.env.CLAUDE_CODE_OAUTH_TOKEN
195
+ || (credReader ? credReader('claude') : _claudeCodeOAuthToken());
196
+ if (oauthToken) return fetchAnthropicModels({ oauthToken, fetchImpl: deps?.fetchImpl });
197
+ throw new Error('claude-cli model listing needs a credential: set ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN (on macOS the `claude` login lives in the Keychain, which is not readable here)');
198
+ }
199
+ if (providerId === 'gemini-cli') {
200
+ const apiKey = key('gemini-cli') || key('gemini') || process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY || '';
201
+ if (apiKey) return fetchGeminiModels({ apiKey, fetchImpl: deps?.fetchImpl });
202
+ throw new Error('gemini-cli model listing needs a credential: set GEMINI_API_KEY or GOOGLE_API_KEY (the `gemini` CLI login token cannot list models)');
203
+ }
204
+ if (providerId === 'codex-cli') {
205
+ const apiKey = key('codex-cli') || key('openai') || process.env.OPENAI_API_KEY
206
+ || (credReader ? credReader('codex') : _codexStoredApiKey()) || '';
207
+ if (apiKey) {
208
+ const { fetchOpenAICompatModels } = await import('./openai_compat.mjs');
209
+ return fetchOpenAICompatModels({ baseUrl: 'https://api.openai.com/v1', apiKey, fetch: deps?.fetchImpl });
210
+ }
211
+ throw new Error('codex-cli model listing needs a credential: set OPENAI_API_KEY (a ChatGPT-plan `codex` login has no platform API key)');
212
+ }
213
+ const c = modelCatalogueFor(deps);
81
214
  if (!c) {
82
- throw new Error(`provider "${providerId}" does not expose an OpenAI-compatible /v1/models endpoint`);
215
+ throw new Error(`provider "${providerId}" does not expose a model catalogue endpoint`);
83
216
  }
84
217
  const { fetchOpenAICompatModels } = await import('./openai_compat.mjs');
85
- return fetchOpenAICompatModels({ baseUrl: c.baseUrl, apiKey: c.apiKey });
218
+ return fetchOpenAICompatModels({ baseUrl: c.baseUrl, apiKey: c.apiKey, fetch: deps?.fetchImpl });
86
219
  }
@@ -12,9 +12,12 @@ import { openaiProvider } from './openai.mjs';
12
12
  import { ollamaProvider } from './ollama.mjs';
13
13
  import { geminiProvider } from './gemini.mjs';
14
14
  import { claudeCliProvider } from './claude_cli.mjs';
15
+ import { geminiCliProvider } from './gemini_cli.mjs';
16
+ import { codexCliProvider } from './codex_cli.mjs';
15
17
  import { hasClaudeCliSession } from './claude_cli_detect.mjs';
16
18
  import { makeOpenAICompatProvider, fetchOpenAICompatModels } from './openai_compat.mjs';
17
19
  import { makeOrchestratorProvider } from './orchestrator.mjs';
20
+ import { OPENAI_COMPAT_BUILTINS } from './compat_vendors.mjs';
18
21
 
19
22
  /**
20
23
  * @typedef {{ role: 'user'|'assistant'|'system', content: string }} ChatMessage
@@ -116,163 +119,10 @@ export function resolveTrainer(cfg, opts = {}) {
116
119
  return { provider: t.provider, model: t.model || chatModel };
117
120
  }
118
121
 
119
- // Built-in OpenAI-compatible vendors. Same wire format → one factory call
120
- // each. The picker treats these like first-class providers so users don't
121
- // have to walk through "+ Add a custom endpoint" for the popular ones.
122
- //
123
- // Each entry must define baseUrl + envKey (the env var the chat path
124
- // consults when no api-key is configured) + suggestedModels (curated list
125
- // shown before the user fetches the live /v1/models catalogue).
126
- //
127
- // Adding a new vendor: drop a row here. The PROVIDERS / PROVIDER_INFO loops
128
- // below pick it up automatically.
129
- export const OPENAI_COMPAT_BUILTINS = {
130
- nim: {
131
- label: 'NVIDIA NIM',
132
- baseUrl: 'https://integrate.api.nvidia.com/v1',
133
- envKey: 'NVIDIA_API_KEY',
134
- altEnvKeys: ['NIM_API_KEY'],
135
- keyPrefix: 'nvapi-',
136
- docs: 'NVIDIA NIM hosted catalogue (Llama 3.x, Nemotron, DeepSeek-R1, Mixtral, Phi-3, Qwen, etc.). Auth: NVIDIA_API_KEY env var or in-app api-key. Endpoint speaks the OpenAI v1 wire format.',
137
- defaultModel: 'meta/llama-3.1-405b-instruct',
138
- suggestedModels: [
139
- 'meta/llama-3.1-405b-instruct',
140
- 'meta/llama-3.1-70b-instruct',
141
- 'meta/llama-3.1-8b-instruct',
142
- 'nvidia/llama-3.1-nemotron-70b-instruct',
143
- 'nvidia/nemotron-mini-4b-instruct',
144
- 'nvidia/llama-3.3-nemotron-super-49b-v1',
145
- 'mistralai/mistral-nemo-12b-instruct',
146
- 'mistralai/mixtral-8x22b-instruct-v0.1',
147
- 'microsoft/phi-3-medium-4k-instruct',
148
- 'deepseek-ai/deepseek-r1',
149
- 'qwen/qwen2.5-7b-instruct',
150
- 'qwen/qwen2.5-coder-32b-instruct',
151
- ],
152
- },
153
- openrouter: {
154
- label: 'OpenRouter',
155
- baseUrl: 'https://openrouter.ai/api/v1',
156
- envKey: 'OPENROUTER_API_KEY',
157
- keyPrefix: 'sk-or-',
158
- docs: 'OpenRouter unified gateway — 200+ models behind one OpenAI-compatible endpoint. Auth: OPENROUTER_API_KEY env var or in-app api-key. Uses x-title/HTTP-Referer headers for attribution.',
159
- defaultModel: 'anthropic/claude-3.5-sonnet',
160
- headers: { 'http-referer': 'https://github.com/cmblir/lazyclaude', 'x-title': 'lazyclaw' },
161
- suggestedModels: [
162
- 'anthropic/claude-3.5-sonnet',
163
- 'anthropic/claude-3-opus',
164
- 'openai/gpt-4o',
165
- 'openai/gpt-4o-mini',
166
- 'openai/o1-preview',
167
- 'meta-llama/llama-3.1-405b-instruct',
168
- 'meta-llama/llama-3.3-70b-instruct',
169
- 'google/gemini-2.0-flash-exp:free',
170
- 'google/gemini-pro-1.5',
171
- 'deepseek/deepseek-chat',
172
- 'deepseek/deepseek-r1',
173
- 'qwen/qwen-2.5-coder-32b-instruct',
174
- 'mistralai/mistral-large',
175
- ],
176
- },
177
- groq: {
178
- label: 'Groq',
179
- baseUrl: 'https://api.groq.com/openai/v1',
180
- envKey: 'GROQ_API_KEY',
181
- keyPrefix: 'gsk_',
182
- docs: 'Groq LPU inference — fastest-token-per-second tier for Llama / Mixtral / Gemma. Auth: GROQ_API_KEY env var or in-app api-key.',
183
- defaultModel: 'llama-3.3-70b-versatile',
184
- suggestedModels: [
185
- 'llama-3.3-70b-versatile',
186
- 'llama-3.1-70b-versatile',
187
- 'llama-3.1-8b-instant',
188
- 'llama-3.2-90b-vision-preview',
189
- 'mixtral-8x7b-32768',
190
- 'gemma2-9b-it',
191
- 'qwen-2.5-coder-32b',
192
- 'qwen-2.5-32b',
193
- 'deepseek-r1-distill-llama-70b',
194
- ],
195
- },
196
- together: {
197
- label: 'Together AI',
198
- baseUrl: 'https://api.together.xyz/v1',
199
- envKey: 'TOGETHER_API_KEY',
200
- docs: 'Together AI hosted inference for open-weight models (Llama, Mixtral, Qwen, DeepSeek, etc.). Auth: TOGETHER_API_KEY env var or in-app api-key.',
201
- defaultModel: 'meta-llama/Llama-3.3-70B-Instruct-Turbo',
202
- suggestedModels: [
203
- 'meta-llama/Llama-3.3-70B-Instruct-Turbo',
204
- 'meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo',
205
- 'meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo',
206
- 'meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo',
207
- 'mistralai/Mixtral-8x22B-Instruct-v0.1',
208
- 'mistralai/Mixtral-8x7B-Instruct-v0.1',
209
- 'Qwen/Qwen2.5-72B-Instruct-Turbo',
210
- 'Qwen/Qwen2.5-Coder-32B-Instruct',
211
- 'deepseek-ai/DeepSeek-V3',
212
- 'deepseek-ai/DeepSeek-R1',
213
- ],
214
- },
215
- xai: {
216
- label: 'xAI (Grok)',
217
- baseUrl: 'https://api.x.ai/v1',
218
- envKey: 'XAI_API_KEY',
219
- altEnvKeys: ['GROK_API_KEY'],
220
- keyPrefix: 'xai-',
221
- docs: 'xAI Grok models. Auth: XAI_API_KEY env var or in-app api-key.',
222
- defaultModel: 'grok-2-latest',
223
- suggestedModels: [
224
- 'grok-2-latest',
225
- 'grok-2-1212',
226
- 'grok-2-vision-1212',
227
- 'grok-beta',
228
- 'grok-vision-beta',
229
- ],
230
- },
231
- deepseek: {
232
- label: 'DeepSeek',
233
- baseUrl: 'https://api.deepseek.com/v1',
234
- envKey: 'DEEPSEEK_API_KEY',
235
- keyPrefix: 'sk-',
236
- docs: 'DeepSeek (deepseek-chat / deepseek-reasoner). Auth: DEEPSEEK_API_KEY env var or in-app api-key.',
237
- defaultModel: 'deepseek-chat',
238
- suggestedModels: [
239
- 'deepseek-chat',
240
- 'deepseek-reasoner',
241
- 'deepseek-coder',
242
- ],
243
- },
244
- mistral: {
245
- label: 'Mistral La Plateforme',
246
- baseUrl: 'https://api.mistral.ai/v1',
247
- envKey: 'MISTRAL_API_KEY',
248
- docs: 'Mistral La Plateforme (mistral-large, codestral, ministral, pixtral). Auth: MISTRAL_API_KEY env var or in-app api-key.',
249
- defaultModel: 'mistral-large-latest',
250
- suggestedModels: [
251
- 'mistral-large-latest',
252
- 'mistral-small-latest',
253
- 'codestral-latest',
254
- 'ministral-8b-latest',
255
- 'ministral-3b-latest',
256
- 'pixtral-large-latest',
257
- 'open-mistral-nemo',
258
- ],
259
- },
260
- fireworks: {
261
- label: 'Fireworks AI',
262
- baseUrl: 'https://api.fireworks.ai/inference/v1',
263
- envKey: 'FIREWORKS_API_KEY',
264
- docs: 'Fireworks AI hosted models. Auth: FIREWORKS_API_KEY env var or in-app api-key.',
265
- defaultModel: 'accounts/fireworks/models/llama-v3p3-70b-instruct',
266
- suggestedModels: [
267
- 'accounts/fireworks/models/llama-v3p3-70b-instruct',
268
- 'accounts/fireworks/models/llama-v3p1-405b-instruct',
269
- 'accounts/fireworks/models/qwen2p5-coder-32b-instruct',
270
- 'accounts/fireworks/models/deepseek-r1',
271
- 'accounts/fireworks/models/deepseek-v3',
272
- 'accounts/fireworks/models/mixtral-8x22b-instruct',
273
- ],
274
- },
275
- };
122
+ // Built-in OpenAI-compatible vendor catalogue moved to
123
+ // providers/compat_vendors.mjs (pure data; add new vendors THERE).
124
+ // Re-exported below so existing importers keep working.
125
+ export { OPENAI_COMPAT_BUILTINS } from './compat_vendors.mjs';
276
126
 
277
127
  // Insertion order is the picker order. The list goes first-to-last in
278
128
  // rough "user-familiar / popular" order so a first-time onboard lands
@@ -280,8 +130,11 @@ export const OPENAI_COMPAT_BUILTINS = {
280
130
  // user feedback ("gemini, codex 이런거 먼저 나오게끔").
281
131
  export const PROVIDERS = {
282
132
  // Tier 1 — popular / brand-name vendors users come in looking for.
133
+ // Keyless CLI variants sit next to their API siblings.
283
134
  gemini: geminiProvider,
135
+ 'gemini-cli': geminiCliProvider, // keyless — local `gemini` CLI login
284
136
  openai: openaiProvider, // surfaces gpt-5-codex / gpt-5 / o3-pro etc.
137
+ 'codex-cli': codexCliProvider, // keyless — local `codex` CLI (ChatGPT plan)
285
138
  // Tier 2 — Claude. CLI variant first because it's keyless.
286
139
  'claude-cli': claudeCliProvider,
287
140
  anthropic: anthropicProvider,
@@ -340,7 +193,10 @@ export const PROVIDER_INFO = {
340
193
  endpoint: 'subprocess: claude -p',
341
194
  defaultModel: 'claude-opus-4-7',
342
195
  suggestedModels: [
196
+ 'claude-fable-5',
197
+ 'claude-opus-4-8',
343
198
  'claude-opus-4-7',
199
+ 'claude-opus-4-6',
344
200
  'claude-sonnet-4-6',
345
201
  'claude-haiku-4-5',
346
202
  'opus',
@@ -348,6 +204,22 @@ export const PROVIDER_INFO = {
348
204
  'haiku',
349
205
  ],
350
206
  },
207
+ 'gemini-cli': {
208
+ name: 'gemini-cli',
209
+ requiresApiKey: false,
210
+ docs: 'Google Gemini via the local `gemini` CLI (free Google-account login). No API key — auth flows through whatever account `gemini` is logged in with. Requires @google/gemini-cli installed.',
211
+ endpoint: 'subprocess: gemini -p',
212
+ defaultModel: 'gemini-2.5-pro',
213
+ suggestedModels: ['gemini-2.5-pro', 'gemini-2.5-flash', 'pro', 'flash'],
214
+ },
215
+ 'codex-cli': {
216
+ name: 'codex-cli',
217
+ requiresApiKey: false,
218
+ docs: 'OpenAI via the local `codex` CLI (ChatGPT Plus/Pro subscription). No API key — auth flows through `codex` login. Requires the codex CLI installed.',
219
+ endpoint: 'subprocess: codex exec',
220
+ defaultModel: 'gpt-5-codex',
221
+ suggestedModels: ['gpt-5-codex', 'gpt-5', 'o3'],
222
+ },
351
223
  anthropic: {
352
224
  name: 'anthropic',
353
225
  requiresApiKey: true,
@@ -356,13 +228,13 @@ export const PROVIDER_INFO = {
356
228
  endpoint: 'https://api.anthropic.com/v1/messages',
357
229
  defaultModel: 'claude-opus-4-7',
358
230
  suggestedModels: [
231
+ 'claude-fable-5',
232
+ 'claude-opus-4-8',
359
233
  'claude-opus-4-7',
360
234
  'claude-opus-4-6',
361
235
  'claude-sonnet-4-6',
362
236
  'claude-sonnet-4-5',
363
237
  'claude-haiku-4-5',
364
- 'claude-3-5-sonnet-20241022',
365
- 'claude-3-5-haiku-20241022',
366
238
  ],
367
239
  },
368
240
  openai: {
@@ -395,9 +267,6 @@ export const PROVIDER_INFO = {
395
267
  'gemini-2.5-pro',
396
268
  'gemini-2.5-flash',
397
269
  'gemini-2.0-flash',
398
- 'gemini-2.0-flash-thinking-exp',
399
- 'gemini-1.5-pro',
400
- 'gemini-1.5-flash',
401
270
  ],
402
271
  },
403
272
  ollama: {
package/tui/pickers.mjs CHANGED
@@ -417,13 +417,13 @@ export async function _arrowMenu({ title, subtitle, footer, items, defaultIdx =
417
417
  // here (rather than registry.mjs) because it's a UX concept, not
418
418
  // an intrinsic provider attribute.
419
419
  export function _providerFamilies() {
420
- // Membership (api/cli/mock, orchestrator excluded) is shared with the Ink
421
- // picker via tui/provider_families.mjs so both paths bucket identically.
422
- // The ANSI tags below are readline-specific, so they're applied here.
420
+ // Membership (api/cli/meta/mock) is shared with the Ink picker via
421
+ // tui/provider_families.mjs; the ANSI tags below are readline-specific.
423
422
  const b = _bucketProviders(getRegistry());
424
423
  return {
425
424
  api: { label: 'API key', desc: 'paste an sk-... key during setup', tag: '\x1b[38;5;245m[needs key]\x1b[0m', members: b.api },
426
425
  cli: { label: 'CLI / Local', desc: 'keyless — uses an existing CLI login or a local daemon', tag: '\x1b[38;5;208m[no key]\x1b[0m', members: b.cli },
426
+ meta: { label: 'Multi-agent', desc: 'orchestrator — fan a task out to a planner + workers (advanced)', tag: '\x1b[38;5;245m[meta]\x1b[0m', members: b.meta },
427
427
  mock: { label: 'Mock', desc: 'offline echo, only useful for testing', tag: '\x1b[38;5;245m[test]\x1b[0m', members: b.mock },
428
428
  };
429
429
  }
@@ -5,17 +5,18 @@
5
5
  // (API key / CLI-Local / Mock) as shared, unit-testable data so both the
6
6
  // readline picker (cli.mjs) and the Ink dispatcher use one bucketing rule.
7
7
 
8
- // Bucket every registered provider into one of three auth families.
9
- // orchestrator is excluded entirely (strictly opt-in; never a wizard
10
- // default see cli.mjs v5.3.2 note). Returns { api:[], cli:[], mock:[] }
11
- // of provider-id strings.
8
+ // Bucket every registered provider into one of four auth families.
9
+ // orchestrator sits in its own `meta` family visible and pickable, but
10
+ // never a wizard default (the cursor always starts on api/cli see the
11
+ // cli.mjs v5.3.2 note). Returns { api:[], cli:[], mock:[], meta:[] } of
12
+ // provider-id strings.
12
13
  export function bucketProviders(registry) {
13
14
  const info = (registry && registry.PROVIDER_INFO) || {};
14
15
  const all = Object.keys((registry && registry.PROVIDERS) || {});
15
- const out = { api: [], cli: [], mock: [] };
16
+ const out = { api: [], cli: [], mock: [], meta: [] };
16
17
  for (const name of all) {
17
18
  if (name === 'mock') out.mock.push(name);
18
- else if (name === 'orchestrator') continue;
19
+ else if (name === 'orchestrator') out.meta.push(name);
19
20
  else if ((info[name] || {}).requiresApiKey) out.api.push(name);
20
21
  else out.cli.push(name);
21
22
  }
@@ -29,6 +30,7 @@ export function providerFamilies(registry) {
29
30
  return {
30
31
  api: { id: 'api', label: 'API key', desc: 'paste an sk-... key', tag: 'needs key', members: b.api },
31
32
  cli: { id: 'cli', label: 'CLI / Local', desc: 'keyless — an existing CLI login or a local daemon', tag: 'no key', members: b.cli },
33
+ meta: { id: 'meta', label: 'Multi-agent', desc: 'orchestrator — fan a task out to a planner + workers (advanced)', tag: 'meta', members: b.meta },
32
34
  mock: { id: 'mock', label: 'Mock', desc: 'offline echo, only useful for testing', tag: 'test', members: b.mock },
33
35
  };
34
36
  }