lazyclaw 3.99.13 → 3.99.15
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/README.md +53 -3
- package/cli.mjs +220 -60
- package/package.json +1 -1
- package/providers/registry.mjs +236 -3
package/README.md
CHANGED
|
@@ -54,6 +54,54 @@ lazyclaw onboard --non-interactive --provider openai \
|
|
|
54
54
|
|
|
55
55
|
`onboard` only prompts for an api-key when the picked provider's `requiresApiKey` is true (the picker labels each row `[subscription]` / `[api key]` / `[no key]` so the choice is explicit).
|
|
56
56
|
|
|
57
|
+
### Built-in OpenAI-compatible vendors
|
|
58
|
+
|
|
59
|
+
Eight popular OpenAI-compatible services ship as first-class providers — pick one in the setup picker (no `+ Add custom` walkthrough needed) or set the matching environment variable and they Just Work:
|
|
60
|
+
|
|
61
|
+
| Provider | Models include | Env var |
|
|
62
|
+
|---|---|---|
|
|
63
|
+
| `nim` | `meta/llama-3.1-405b-instruct`, `nvidia/llama-3.1-nemotron-70b-instruct`, `deepseek-ai/deepseek-r1`, `mistralai/mixtral-8x22b-instruct-v0.1` | `NVIDIA_API_KEY` (or `NIM_API_KEY`) |
|
|
64
|
+
| `openrouter` | `anthropic/claude-3.5-sonnet`, `openai/gpt-4o`, `meta-llama/llama-3.1-405b-instruct`, `deepseek/deepseek-r1` | `OPENROUTER_API_KEY` |
|
|
65
|
+
| `groq` | `llama-3.3-70b-versatile`, `mixtral-8x7b-32768`, `deepseek-r1-distill-llama-70b` | `GROQ_API_KEY` |
|
|
66
|
+
| `together` | `meta-llama/Llama-3.3-70B-Instruct-Turbo`, `Qwen/Qwen2.5-72B-Instruct-Turbo`, `deepseek-ai/DeepSeek-V3` | `TOGETHER_API_KEY` |
|
|
67
|
+
| `xai` | `grok-2-latest`, `grok-2-vision-1212` | `XAI_API_KEY` (or `GROK_API_KEY`) |
|
|
68
|
+
| `deepseek` | `deepseek-chat`, `deepseek-reasoner` | `DEEPSEEK_API_KEY` |
|
|
69
|
+
| `mistral` | `mistral-large-latest`, `codestral-latest`, `pixtral-large-latest` | `MISTRAL_API_KEY` |
|
|
70
|
+
| `fireworks` | `accounts/fireworks/models/llama-v3p3-70b-instruct`, `…/deepseek-r1` | `FIREWORKS_API_KEY` |
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# NVIDIA NIM via env var — no `lazyclaw onboard` needed
|
|
74
|
+
export NVIDIA_API_KEY=nvapi-...
|
|
75
|
+
lazyclaw chat --provider nim --model meta/llama-3.1-405b-instruct
|
|
76
|
+
|
|
77
|
+
# Or commit the choice to ~/.lazyclaw/config.json
|
|
78
|
+
lazyclaw onboard --non-interactive --provider nim \
|
|
79
|
+
--model nvidia/llama-3.1-nemotron-70b-instruct --api-key nvapi-...
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Need a vendor that's **not** built-in? `+ Add a custom OpenAI-compatible endpoint…` inside the setup picker (or `lazyclaw providers add <name> --base-url <url>`) still works for vLLM / LM Studio / private gateways / anything else that speaks the OpenAI v1 wire format.
|
|
83
|
+
|
|
84
|
+
## Launcher (no-arg `lazyclaw`)
|
|
85
|
+
|
|
86
|
+
Running `lazyclaw` with no subcommand drops into an arrow-key launcher with every subcommand laid out as a menu. Navigation:
|
|
87
|
+
|
|
88
|
+
| Key | What it does |
|
|
89
|
+
|---|---|
|
|
90
|
+
| `↑` / `↓` / `Home` / `End` / `PgUp` / `PgDn` | Move the selection |
|
|
91
|
+
| `Enter` | Run the highlighted item |
|
|
92
|
+
| `q` / `Esc` / `Ctrl-C` | Leave lazyclaw |
|
|
93
|
+
| `/` | Open an inline slash-command prompt |
|
|
94
|
+
|
|
95
|
+
Slash commands at the launcher (typed after `/`):
|
|
96
|
+
|
|
97
|
+
| Slash | What it does |
|
|
98
|
+
|---|---|
|
|
99
|
+
| `/exit` / `/quit` | Leave lazyclaw |
|
|
100
|
+
| `/help` | List launcher slash commands inline |
|
|
101
|
+
| `/version` | Print version + node + platform |
|
|
102
|
+
|
|
103
|
+
The slash buffer lives just below the menu — backspace edits it, deleting past `/` returns to menu mode, and `Esc` cancels slash mode without leaving lazyclaw.
|
|
104
|
+
|
|
57
105
|
## Interactive chat
|
|
58
106
|
|
|
59
107
|
```bash
|
|
@@ -88,12 +136,14 @@ Slash commands inside the REPL:
|
|
|
88
136
|
|---|---|
|
|
89
137
|
| `/help` | List slash commands |
|
|
90
138
|
| `/status` | Print provider + model + masked key |
|
|
91
|
-
| `/provider
|
|
92
|
-
| `/
|
|
139
|
+
| `/provider` | Open the family / provider / model arrow picker |
|
|
140
|
+
| `/provider X` | Switch active provider directly by name |
|
|
141
|
+
| `/model` | Open the per-provider model picker (type-filter + live `/v1/models` fetch) |
|
|
142
|
+
| `/model X` | Switch model directly. Accepts unified `provider/model` form |
|
|
93
143
|
| `/skill a,b` | Replace the system prompt with a composition of named skills |
|
|
94
144
|
| `/usage` | Message count + chars + cumulative token totals |
|
|
95
145
|
| `/new` / `/reset` | Wipe history and start over |
|
|
96
|
-
| `/exit` |
|
|
146
|
+
| `/exit` | Leave the chat REPL (returns to the launcher when chat was opened from it) |
|
|
97
147
|
|
|
98
148
|
**Cursor-style ghost autocomplete**: type `/` and the longest matching slash command appears in dim grey after the cursor. **`→`** accepts; **`Tab`** cycles. **Ctrl-C** during a streaming reply aborts that turn (not the whole process); **Ctrl-C** at an empty prompt exits.
|
|
99
149
|
|
package/cli.mjs
CHANGED
|
@@ -33,19 +33,26 @@ function writeConfig(cfg) {
|
|
|
33
33
|
// without forcing the dynamic import on every hot-path call.
|
|
34
34
|
// 1. cfg.authProfiles[provider] active label, if set
|
|
35
35
|
// 2. first profile in the array
|
|
36
|
-
// 3.
|
|
36
|
+
// 3. customProviders[<provider>].apiKey (custom OpenAI-compat entries)
|
|
37
|
+
// 4. PROVIDER_INFO[<provider>].envKey / altEnvKeys env var (built-in
|
|
38
|
+
// OpenAI-compat: nim → NVIDIA_API_KEY, openrouter → OPENROUTER_API_KEY, …)
|
|
39
|
+
// 5. legacy single `cfg["api-key"]` (pre-v3.93 configs)
|
|
37
40
|
function _resolveAuthKey(cfg, provider) {
|
|
38
41
|
const arr = (cfg.authProfiles || {})[provider] || [];
|
|
39
42
|
const active = (cfg.authActiveProfile || {})[provider];
|
|
40
43
|
const hit = arr.find((p) => p && p.label === active) || arr[0];
|
|
41
44
|
if (hit?.key) return hit.key;
|
|
42
|
-
// Custom OpenAI-compatible providers store their api-key inline in the
|
|
43
|
-
// customProviders[] entry. Honour that before falling back to the
|
|
44
|
-
// legacy single-key cfg['api-key'].
|
|
45
45
|
const custom = Array.isArray(cfg.customProviders)
|
|
46
46
|
? cfg.customProviders.find((p) => p && p.name === provider)
|
|
47
47
|
: null;
|
|
48
48
|
if (custom?.apiKey) return custom.apiKey;
|
|
49
|
+
// Built-in OpenAI-compat env var fallback. Skipped silently when the
|
|
50
|
+
// registry module isn't loaded yet (every chat / agent path calls
|
|
51
|
+
// ensureRegistry() before _resolveAuthKey, so this is just defence-in-depth).
|
|
52
|
+
if (_registryMod && typeof _registryMod.resolveBuiltinEnvKey === 'function') {
|
|
53
|
+
const envHit = _registryMod.resolveBuiltinEnvKey(provider);
|
|
54
|
+
if (envHit) return envHit;
|
|
55
|
+
}
|
|
49
56
|
return cfg['api-key'] || '';
|
|
50
57
|
}
|
|
51
58
|
|
|
@@ -1724,12 +1731,19 @@ async function _pickProviderInteractive() {
|
|
|
1724
1731
|
while (!family) {
|
|
1725
1732
|
const familyItems = Object.entries(families)
|
|
1726
1733
|
.filter(([, b]) => b.members.length > 0)
|
|
1727
|
-
.map(([id, b]) =>
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1734
|
+
.map(([id, b]) => {
|
|
1735
|
+
// Show member count + a few names instead of the full list — the
|
|
1736
|
+
// API-key family alone now has 12 vendors and joining all of them
|
|
1737
|
+
// produced an unreadable line.
|
|
1738
|
+
const preview = b.members.slice(0, 3).join(' / ');
|
|
1739
|
+
const more = b.members.length > 3 ? ` … (+${b.members.length - 3} more)` : '';
|
|
1740
|
+
return {
|
|
1741
|
+
id,
|
|
1742
|
+
label: b.label,
|
|
1743
|
+
desc: `${b.desc} · ${preview}${more}`,
|
|
1744
|
+
tag: b.tag,
|
|
1745
|
+
};
|
|
1746
|
+
});
|
|
1733
1747
|
const picked = await _arrowMenu({
|
|
1734
1748
|
title: 'LazyClaw setup — Step 1 of 3: pick how you want to auth',
|
|
1735
1749
|
subtitle: 'API: bring your own key · CLI/Local: use what\'s already on this machine · Mock: offline test',
|
|
@@ -1745,14 +1759,21 @@ async function _pickProviderInteractive() {
|
|
|
1745
1759
|
const memberNames = families[family.id].members;
|
|
1746
1760
|
const provItems = memberNames.map((name) => {
|
|
1747
1761
|
const meta = info[name] || {};
|
|
1748
|
-
const models = (meta.suggestedModels || []).slice(0, 4).join(' · ') || '(default)';
|
|
1749
1762
|
const isCustom = !!meta.custom;
|
|
1763
|
+
const isBuiltinCompat = !!meta.builtinOpenAICompat;
|
|
1764
|
+
// Step-2 desc used to preview four suggested model ids per provider.
|
|
1765
|
+
// That made the row read like "gemini · models: gemini-2.5-pro ·
|
|
1766
|
+
// gemini-2.5-flash · gemini-2.0-flash · gemini-2.0-flash-thinking-exp",
|
|
1767
|
+
// which is too dense and partly redundant — step 3 already shows the
|
|
1768
|
+
// full curated list. Keep the row to a vendor label + endpoint hint.
|
|
1769
|
+
let desc = '';
|
|
1770
|
+
if (isCustom) desc = `custom · ${meta.baseUrl || ''}`;
|
|
1771
|
+
else if (isBuiltinCompat) desc = meta.label || meta.baseUrl || '';
|
|
1772
|
+
else if (meta.label && meta.label !== name) desc = meta.label;
|
|
1750
1773
|
return {
|
|
1751
1774
|
id: name,
|
|
1752
1775
|
label: name,
|
|
1753
|
-
desc
|
|
1754
|
-
? `custom · ${meta.baseUrl || ''}`
|
|
1755
|
-
: `models: ${models}`,
|
|
1776
|
+
desc,
|
|
1756
1777
|
tag: isCustom
|
|
1757
1778
|
? '\x1b[38;5;213m[custom]\x1b[0m'
|
|
1758
1779
|
: (meta.requiresApiKey ? '\x1b[38;5;245m[api key]\x1b[0m' : '\x1b[38;5;208m[no key]\x1b[0m'),
|
|
@@ -1798,17 +1819,58 @@ async function _pickProviderInteractive() {
|
|
|
1798
1819
|
}
|
|
1799
1820
|
|
|
1800
1821
|
// ── Step 3 — model ────────────────────────────────────────────
|
|
1801
|
-
const
|
|
1822
|
+
const picked = await _pickModelInteractive(provider.id, {
|
|
1823
|
+
titlePrefix: 'LazyClaw setup — Step 3 of 3:',
|
|
1824
|
+
onBack: 'restart',
|
|
1825
|
+
});
|
|
1826
|
+
if (picked === 'CANCEL') return null;
|
|
1827
|
+
if (picked === 'BACK') return _pickProviderInteractive();
|
|
1828
|
+
return { provider: provider.id, model: picked };
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
// Pause the chat REPL's readline + ghost-autocomplete while a sub-picker
|
|
1832
|
+
// (provider / model arrow menu) takes over the terminal. The sub-picker
|
|
1833
|
+
// installs its own `keypress` listener and toggles raw mode; the chat's
|
|
1834
|
+
// readline would race it for stdin if we left it active. After `body`
|
|
1835
|
+
// returns we re-emit keypress events, restore raw mode, and re-prompt
|
|
1836
|
+
// so the chat resumes cleanly. `body` is awaited — exceptions propagate.
|
|
1837
|
+
async function _pauseChatForSubMenu(rl, ghost, body) {
|
|
1838
|
+
if (ghost && typeof ghost.suspend === 'function') ghost.suspend();
|
|
1839
|
+
try { rl.pause(); } catch (_) {}
|
|
1840
|
+
// Drop the readline keypress hook so the picker's own listener has
|
|
1841
|
+
// sole ownership while it's open. We re-arm it on the way out.
|
|
1842
|
+
if (process.stdin.setRawMode) {
|
|
1843
|
+
try { process.stdin.setRawMode(false); } catch (_) {}
|
|
1844
|
+
}
|
|
1845
|
+
try {
|
|
1846
|
+
await body();
|
|
1847
|
+
} finally {
|
|
1848
|
+
const readline = await import('node:readline');
|
|
1849
|
+
try { readline.emitKeypressEvents(process.stdin); } catch (_) {}
|
|
1850
|
+
if (process.stdin.setRawMode && process.stdin.isTTY) {
|
|
1851
|
+
try { process.stdin.setRawMode(false); } catch (_) {}
|
|
1852
|
+
}
|
|
1853
|
+
process.stdin.resume();
|
|
1854
|
+
if (process.stdin.ref) process.stdin.ref();
|
|
1855
|
+
if (ghost && typeof ghost.resume === 'function') ghost.resume();
|
|
1856
|
+
try { rl.resume(); } catch (_) {}
|
|
1857
|
+
try { rl.prompt(); } catch (_) {}
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
// Standalone model picker for the chat REPL's `/model` slash. Returns
|
|
1862
|
+
// the chosen model id (string), 'BACK', or 'CANCEL'. Falls through to
|
|
1863
|
+
// null when the provider has no curated models and no live-fetch surface
|
|
1864
|
+
// (mock) — the caller should treat that as "use the provider default".
|
|
1865
|
+
async function _pickModelInteractive(providerId, opts = {}) {
|
|
1866
|
+
const info = _registryMod.PROVIDER_INFO || {};
|
|
1867
|
+
const meta = info[providerId] || {};
|
|
1802
1868
|
const baseModels = Array.isArray(meta.suggestedModels) ? meta.suggestedModels.slice() : [];
|
|
1803
1869
|
const isCustom = !!meta.custom;
|
|
1804
|
-
const
|
|
1870
|
+
const isBuiltinCompat = !!meta.builtinOpenAICompat;
|
|
1871
|
+
const supportsLiveFetch = !!meta.baseUrl || providerId === 'openai' || providerId === 'ollama' || isBuiltinCompat;
|
|
1805
1872
|
|
|
1806
|
-
if (!baseModels.length && !supportsLiveFetch)
|
|
1807
|
-
// Provider has no curated models AND no live-fetch surface (mock) —
|
|
1808
|
-
// return without a model so the underlying call uses the provider
|
|
1809
|
-
// default.
|
|
1810
|
-
return { provider: provider.id, model: null };
|
|
1811
|
-
}
|
|
1873
|
+
if (!baseModels.length && !supportsLiveFetch) return null;
|
|
1812
1874
|
|
|
1813
1875
|
let dynamicModels = [];
|
|
1814
1876
|
while (true) {
|
|
@@ -1818,7 +1880,7 @@ async function _pickProviderInteractive() {
|
|
|
1818
1880
|
modelItems.unshift({
|
|
1819
1881
|
id: '__fetch_models__',
|
|
1820
1882
|
label: '↻ Fetch live model list from /v1/models',
|
|
1821
|
-
desc: isCustom ? `GET ${meta.baseUrl}/models` : 'pulls the up-to-date catalogue from the provider',
|
|
1883
|
+
desc: isCustom || isBuiltinCompat ? `GET ${meta.baseUrl}/models` : 'pulls the up-to-date catalogue from the provider',
|
|
1822
1884
|
tag: '\x1b[38;5;245m[live]\x1b[0m',
|
|
1823
1885
|
});
|
|
1824
1886
|
}
|
|
@@ -1832,24 +1894,25 @@ async function _pickProviderInteractive() {
|
|
|
1832
1894
|
const defaultIdx = supportsLiveFetch
|
|
1833
1895
|
? Math.max(0, 1 + allModels.indexOf(meta.defaultModel || allModels[0]))
|
|
1834
1896
|
: Math.max(0, allModels.indexOf(meta.defaultModel || allModels[0]));
|
|
1897
|
+
const titlePrefix = opts.titlePrefix ? `${opts.titlePrefix} ` : '';
|
|
1835
1898
|
const picked = await _arrowMenu({
|
|
1836
|
-
title:
|
|
1899
|
+
title: `${titlePrefix}pick a model for ${providerId}`,
|
|
1837
1900
|
subtitle: `Type to filter ${allModels.length} model(s). Enter to confirm. Backspace clears one char, Ctrl+U clears the filter.`,
|
|
1838
1901
|
items: modelItems,
|
|
1839
1902
|
defaultIdx,
|
|
1840
1903
|
searchable: true,
|
|
1841
1904
|
});
|
|
1842
|
-
if (picked === 'CANCEL') return
|
|
1843
|
-
if (picked === 'BACK') return
|
|
1905
|
+
if (picked === 'CANCEL') return 'CANCEL';
|
|
1906
|
+
if (picked === 'BACK') return 'BACK';
|
|
1844
1907
|
if (picked.id === '__custom_model__') {
|
|
1845
|
-
const typed = (await _quickPrompt(` model id for ${
|
|
1908
|
+
const typed = (await _quickPrompt(` model id for ${providerId}: `)).trim();
|
|
1846
1909
|
if (!typed) continue;
|
|
1847
|
-
return
|
|
1910
|
+
return typed;
|
|
1848
1911
|
}
|
|
1849
1912
|
if (picked.id === '__fetch_models__') {
|
|
1850
1913
|
try {
|
|
1851
|
-
process.stdout.write(`\n fetching ${
|
|
1852
|
-
const fetched = await _fetchModelsForProvider(
|
|
1914
|
+
process.stdout.write(`\n fetching ${providerId} model list…\n`);
|
|
1915
|
+
const fetched = await _fetchModelsForProvider(providerId);
|
|
1853
1916
|
if (!fetched.length) {
|
|
1854
1917
|
process.stdout.write(` ${'\x1b[33m'}no models returned${'\x1b[0m'} — falling back to the suggested list.\n`);
|
|
1855
1918
|
await _quickPrompt(' press Enter to continue ');
|
|
@@ -1864,7 +1927,7 @@ async function _pickProviderInteractive() {
|
|
|
1864
1927
|
}
|
|
1865
1928
|
continue;
|
|
1866
1929
|
}
|
|
1867
|
-
return
|
|
1930
|
+
return picked.id;
|
|
1868
1931
|
}
|
|
1869
1932
|
}
|
|
1870
1933
|
|
|
@@ -1878,6 +1941,12 @@ function _modelCatalogueFor(providerId) {
|
|
|
1878
1941
|
const entry = (cfg.customProviders || []).find((p) => p && p.name === providerId) || {};
|
|
1879
1942
|
return { baseUrl: meta.baseUrl, apiKey: entry.apiKey || cfg['api-key'] || '' };
|
|
1880
1943
|
}
|
|
1944
|
+
// Built-in OpenAI-compatible vendors (nim / openrouter / groq / together /
|
|
1945
|
+
// xai / deepseek / mistral / fireworks). The registry exposes a baseUrl
|
|
1946
|
+
// and the auth-key resolver already knows about the env-var fallback.
|
|
1947
|
+
if (meta.builtinOpenAICompat && meta.baseUrl) {
|
|
1948
|
+
return { baseUrl: meta.baseUrl, apiKey: _resolveAuthKey(cfg, providerId) };
|
|
1949
|
+
}
|
|
1881
1950
|
if (providerId === 'openai') {
|
|
1882
1951
|
return { baseUrl: 'https://api.openai.com/v1', apiKey: _resolveAuthKey(cfg, 'openai') };
|
|
1883
1952
|
}
|
|
@@ -1915,7 +1984,7 @@ async function _addCustomProviderInteractive() {
|
|
|
1915
1984
|
process.stdout.write(dim(' · Groq https://api.groq.com/openai/v1') + '\n');
|
|
1916
1985
|
process.stdout.write(dim(' · vLLM / LM Studio http://localhost:8000/v1') + '\n\n');
|
|
1917
1986
|
|
|
1918
|
-
const { validateCustomProviderName, registerCustomProviders, fetchOpenAICompatModels } = _registryMod;
|
|
1987
|
+
const { validateCustomProviderName, registerCustomProviders, fetchOpenAICompatModels, isBuiltinOpenAICompatName } = _registryMod;
|
|
1919
1988
|
let name;
|
|
1920
1989
|
while (true) {
|
|
1921
1990
|
const raw = (await _quickPrompt(` ${bold('name')} ${dim('(short id, e.g. "nim", "openrouter"):')} `)).trim();
|
|
@@ -1923,8 +1992,24 @@ async function _addCustomProviderInteractive() {
|
|
|
1923
1992
|
process.stdout.write(dim(' cancelled — back to the picker.\n'));
|
|
1924
1993
|
return null;
|
|
1925
1994
|
}
|
|
1926
|
-
try { name = validateCustomProviderName(raw);
|
|
1927
|
-
catch (e) {
|
|
1995
|
+
try { name = validateCustomProviderName(raw); }
|
|
1996
|
+
catch (e) {
|
|
1997
|
+
process.stdout.write(` \x1b[33m${e.message}\x1b[0m — try again.\n`);
|
|
1998
|
+
continue;
|
|
1999
|
+
}
|
|
2000
|
+
// OpenAI-compat builtins (nim / openrouter / groq / …) can be overridden
|
|
2001
|
+
// by a custom entry of the same name — both go through
|
|
2002
|
+
// makeOpenAICompatProvider, so the wire format is identical and the
|
|
2003
|
+
// user is just pointing the same alias at a different URL/key. Surface
|
|
2004
|
+
// the override so it isn't a silent surprise.
|
|
2005
|
+
if (typeof isBuiltinOpenAICompatName === 'function' && isBuiltinOpenAICompatName(name)) {
|
|
2006
|
+
process.stdout.write(
|
|
2007
|
+
` \x1b[2mNote: "${name}" is a built-in OpenAI-compatible provider; ` +
|
|
2008
|
+
`your custom entry will override the built-in baseUrl/api-key for this install. ` +
|
|
2009
|
+
`Remove with: lazyclaw providers remove ${name}\x1b[0m\n`
|
|
2010
|
+
);
|
|
2011
|
+
}
|
|
2012
|
+
break;
|
|
1928
2013
|
}
|
|
1929
2014
|
const baseUrlRaw = (await _quickPrompt(` ${bold('baseUrl')} ${dim('(must end in /v1, no trailing slash needed):')} `)).trim();
|
|
1930
2015
|
if (!baseUrlRaw) { process.stdout.write(dim(' cancelled — baseUrl is required.\n')); return null; }
|
|
@@ -2133,10 +2218,28 @@ async function cmdChat(flags = {}) {
|
|
|
2133
2218
|
// `/provider <name>` switches the active provider for subsequent
|
|
2134
2219
|
// turns. The conversation history stays put — the next user
|
|
2135
2220
|
// message goes to the new provider with the existing context.
|
|
2136
|
-
// `/provider` (no arg)
|
|
2221
|
+
// `/provider` (no arg) opens the family/provider/model picker so
|
|
2222
|
+
// the user can switch with arrow keys instead of memorising names.
|
|
2137
2223
|
const arg = line.slice('/provider'.length).trim();
|
|
2138
2224
|
if (!arg) {
|
|
2139
|
-
|
|
2225
|
+
if (!useTerminal) {
|
|
2226
|
+
process.stdout.write(`provider: ${activeProvName}\n`);
|
|
2227
|
+
return true;
|
|
2228
|
+
}
|
|
2229
|
+
await _pauseChatForSubMenu(rl, _ghost, async () => {
|
|
2230
|
+
const picked = await _pickProviderInteractive();
|
|
2231
|
+
if (picked && picked.provider) {
|
|
2232
|
+
const next = lookupProv(picked.provider);
|
|
2233
|
+
if (!next) {
|
|
2234
|
+
process.stdout.write(`unknown provider: ${picked.provider}\n`);
|
|
2235
|
+
return;
|
|
2236
|
+
}
|
|
2237
|
+
activeProvName = picked.provider;
|
|
2238
|
+
prov = next;
|
|
2239
|
+
if (picked.model) activeModel = picked.model;
|
|
2240
|
+
process.stdout.write(`provider → ${activeProvName}${picked.model ? ` · model → ${picked.model}` : ''}\n`);
|
|
2241
|
+
}
|
|
2242
|
+
});
|
|
2140
2243
|
return true;
|
|
2141
2244
|
}
|
|
2142
2245
|
const next = lookupProv(arg);
|
|
@@ -2151,10 +2254,20 @@ async function cmdChat(flags = {}) {
|
|
|
2151
2254
|
}
|
|
2152
2255
|
case '/model': {
|
|
2153
2256
|
// `/model <name>` updates the active model without touching the
|
|
2154
|
-
// provider. `/model` (no arg)
|
|
2257
|
+
// provider. `/model` (no arg) opens the per-provider model picker
|
|
2258
|
+
// — same UX as setup step 3, scoped to the active provider.
|
|
2155
2259
|
const arg = line.slice('/model'.length).trim();
|
|
2156
2260
|
if (!arg) {
|
|
2157
|
-
|
|
2261
|
+
if (!useTerminal) {
|
|
2262
|
+
process.stdout.write(`model: ${activeModel || '(default)'}\n`);
|
|
2263
|
+
return true;
|
|
2264
|
+
}
|
|
2265
|
+
await _pauseChatForSubMenu(rl, _ghost, async () => {
|
|
2266
|
+
const chosen = await _pickModelInteractive(activeProvName, { titlePrefix: 'LazyClaw chat —' });
|
|
2267
|
+
if (chosen === 'CANCEL' || chosen === 'BACK' || !chosen) return;
|
|
2268
|
+
activeModel = chosen;
|
|
2269
|
+
process.stdout.write(`model → ${activeModel}\n`);
|
|
2270
|
+
});
|
|
2158
2271
|
return true;
|
|
2159
2272
|
}
|
|
2160
2273
|
// Honor unified provider/model: `/model anthropic/claude-opus-4-7`
|
|
@@ -2259,18 +2372,12 @@ async function cmdChat(flags = {}) {
|
|
|
2259
2372
|
}
|
|
2260
2373
|
};
|
|
2261
2374
|
|
|
2262
|
-
// Tracks whether the loop ended because the user explicitly typed
|
|
2263
|
-
// /exit (vs. natural EOF / Ctrl-D). When set, cmdChat returns the
|
|
2264
|
-
// 'LAZYCLAW_EXIT' sentinel so cmdLauncher knows to break its outer
|
|
2265
|
-
// menu loop instead of redrawing — /exit means "leave lazyclaw",
|
|
2266
|
-
// not "leave just this chat REPL and bounce back to the menu."
|
|
2267
|
-
let userRequestedExit = false;
|
|
2268
2375
|
try { for await (const line of rl) {
|
|
2269
2376
|
const text = line.trim();
|
|
2270
2377
|
if (!text) { if (useTerminal) rl.prompt(); continue; }
|
|
2271
2378
|
if (text.startsWith('/')) {
|
|
2272
2379
|
const r = await handleSlash(text);
|
|
2273
|
-
if (r === 'EXIT')
|
|
2380
|
+
if (r === 'EXIT') break;
|
|
2274
2381
|
if (useTerminal) rl.prompt();
|
|
2275
2382
|
continue;
|
|
2276
2383
|
}
|
|
@@ -2365,11 +2472,6 @@ async function cmdChat(flags = {}) {
|
|
|
2365
2472
|
try { process.stdin.pause(); } catch (_) {}
|
|
2366
2473
|
try { process.stdin.unref(); } catch (_) {}
|
|
2367
2474
|
}
|
|
2368
|
-
// Sentinel — picked up by cmdLauncher's dispatch loop. Returning
|
|
2369
|
-
// anything else (undefined / EOF) leaves the launcher free to
|
|
2370
|
-
// redraw its menu, which is the right behavior for natural stream
|
|
2371
|
-
// exhaustion (e.g. a script piping prompts on stdin).
|
|
2372
|
-
if (userRequestedExit) return 'LAZYCLAW_EXIT';
|
|
2373
2475
|
}
|
|
2374
2476
|
|
|
2375
2477
|
// Light wrapper around the daemon — meant for users who installed
|
|
@@ -4095,7 +4197,7 @@ async function cmdLauncher() {
|
|
|
4095
4197
|
process.stdout.write(` ${dim('model ·')} ${ok(model)}\n`);
|
|
4096
4198
|
process.stdout.write(` ${dim('config ·')} ${dim(configPath())}\n`);
|
|
4097
4199
|
process.stdout.write('\n');
|
|
4098
|
-
process.stdout.write(` ${dim('↑/↓ to move · Enter to select · q or Esc to quit')}\n\n`);
|
|
4200
|
+
process.stdout.write(` ${dim('↑/↓ to move · Enter to select · / for slash command (e.g. /exit) · q or Esc to quit')}\n\n`);
|
|
4099
4201
|
const rowsAvail = Math.max(items.length, (process.stdout.rows || 30) - 14);
|
|
4100
4202
|
const fromIdx = Math.max(0, Math.min(items.length - rowsAvail, idx - Math.floor(rowsAvail / 2)));
|
|
4101
4203
|
const toIdx = Math.min(items.length, fromIdx + rowsAvail);
|
|
@@ -4108,10 +4210,74 @@ async function cmdLauncher() {
|
|
|
4108
4210
|
process.stdout.write('\n');
|
|
4109
4211
|
};
|
|
4110
4212
|
|
|
4213
|
+
// Slash-command mini prompt rendered just below the menu. Lets users
|
|
4214
|
+
// type `/exit` / `/quit` / `/help` to leave (or get a list of slash
|
|
4215
|
+
// commands) without hunting for the right special key. The menu is
|
|
4216
|
+
// raw-mode and never sees a newline-terminated line, so we accumulate
|
|
4217
|
+
// keystrokes locally instead of round-tripping through readline.
|
|
4218
|
+
let slashBuffer = null; // null = menu mode; string = slash mode (always starts with '/')
|
|
4219
|
+
let slashNotice = ''; // one-line hint shown after the buffer (e.g. "unknown command")
|
|
4220
|
+
const LAUNCHER_SLASH_HELP = [
|
|
4221
|
+
{ cmd: '/exit', help: 'leave lazyclaw' },
|
|
4222
|
+
{ cmd: '/quit', help: 'alias for /exit' },
|
|
4223
|
+
{ cmd: '/help', help: 'list slash commands' },
|
|
4224
|
+
{ cmd: '/version', help: 'print version + node + platform' },
|
|
4225
|
+
];
|
|
4226
|
+
const drawWithSlash = () => {
|
|
4227
|
+
draw();
|
|
4228
|
+
process.stdout.write(` ${dim('slash ›')} ${slashBuffer}`);
|
|
4229
|
+
if (slashNotice) process.stdout.write(` ${slashNotice}`);
|
|
4230
|
+
process.stdout.write('\x1b[?25h'); // show cursor while typing
|
|
4231
|
+
};
|
|
4232
|
+
|
|
4111
4233
|
draw();
|
|
4112
4234
|
const picked = await new Promise((resolve) => {
|
|
4113
|
-
const onKey = (
|
|
4235
|
+
const onKey = (str, key) => {
|
|
4114
4236
|
if (!key) return;
|
|
4237
|
+
|
|
4238
|
+
// ── Slash-command input mode ─────────────────────────────────
|
|
4239
|
+
if (slashBuffer !== null) {
|
|
4240
|
+
if (key.ctrl && key.name === 'c') { cleanup(); resolve({ id: 'quit', argv: null }); return; }
|
|
4241
|
+
if (key.name === 'escape') { slashBuffer = null; slashNotice = ''; draw(); return; }
|
|
4242
|
+
if (key.name === 'return') {
|
|
4243
|
+
const cmd = slashBuffer.trim().toLowerCase();
|
|
4244
|
+
if (cmd === '/exit' || cmd === '/quit') { cleanup(); resolve({ id: 'quit', argv: null }); return; }
|
|
4245
|
+
if (cmd === '/help') {
|
|
4246
|
+
slashBuffer = '/';
|
|
4247
|
+
slashNotice = dim(LAUNCHER_SLASH_HELP.map(c => `${c.cmd} (${c.help})`).join(' · '));
|
|
4248
|
+
drawWithSlash();
|
|
4249
|
+
return;
|
|
4250
|
+
}
|
|
4251
|
+
if (cmd === '/version') {
|
|
4252
|
+
const v = readVersionFromRepo();
|
|
4253
|
+
slashNotice = ok(`v${v} · node ${process.version} · ${process.platform}-${process.arch}`);
|
|
4254
|
+
drawWithSlash();
|
|
4255
|
+
return;
|
|
4256
|
+
}
|
|
4257
|
+
// Unknown command — keep the buffer so the user can edit it
|
|
4258
|
+
// rather than retyping from scratch. Esc / Backspace bails.
|
|
4259
|
+
slashNotice = warn(`unknown — try ${LAUNCHER_SLASH_HELP.map(c => c.cmd).join(' · ')}`);
|
|
4260
|
+
drawWithSlash();
|
|
4261
|
+
return;
|
|
4262
|
+
}
|
|
4263
|
+
if (key.name === 'backspace') {
|
|
4264
|
+
slashNotice = '';
|
|
4265
|
+
if (slashBuffer.length > 1) slashBuffer = slashBuffer.slice(0, -1);
|
|
4266
|
+
else slashBuffer = null;
|
|
4267
|
+
slashBuffer === null ? draw() : drawWithSlash();
|
|
4268
|
+
return;
|
|
4269
|
+
}
|
|
4270
|
+
// Append printable characters. Filter control / meta chords so
|
|
4271
|
+
// Ctrl+L etc. don't pollute the buffer.
|
|
4272
|
+
if (str && str.length === 1 && !key.ctrl && !key.meta && str >= ' ') {
|
|
4273
|
+
slashBuffer += str;
|
|
4274
|
+
slashNotice = '';
|
|
4275
|
+
drawWithSlash();
|
|
4276
|
+
}
|
|
4277
|
+
return;
|
|
4278
|
+
}
|
|
4279
|
+
|
|
4280
|
+
// ── Menu navigation mode ─────────────────────────────────────
|
|
4115
4281
|
if (key.name === 'up') { idx = (idx - 1 + items.length) % items.length; draw(); }
|
|
4116
4282
|
else if (key.name === 'down') { idx = (idx + 1) % items.length; draw(); }
|
|
4117
4283
|
else if (key.name === 'home') { idx = 0; draw(); }
|
|
@@ -4121,6 +4287,7 @@ async function cmdLauncher() {
|
|
|
4121
4287
|
else if (key.name === 'return') { cleanup(); resolve(items[idx]); }
|
|
4122
4288
|
else if (key.ctrl && key.name === 'c') { cleanup(); resolve({ id: 'quit', argv: null }); }
|
|
4123
4289
|
else if (key.name === 'escape' || key.name === 'q') { cleanup(); resolve({ id: 'quit', argv: null }); }
|
|
4290
|
+
else if (str === '/') { slashBuffer = '/'; slashNotice = ''; drawWithSlash(); }
|
|
4124
4291
|
function cleanup() {
|
|
4125
4292
|
process.stdin.off('keypress', onKey);
|
|
4126
4293
|
if (process.stdin.setRawMode) process.stdin.setRawMode(false);
|
|
@@ -4151,18 +4318,11 @@ async function cmdLauncher() {
|
|
|
4151
4318
|
// Dispatch. Errors don't terminate the launcher — they're
|
|
4152
4319
|
// surfaced as a stderr line and the menu redraws. Lets the
|
|
4153
4320
|
// user recover from a transient API hiccup without a relaunch.
|
|
4154
|
-
let dispatchResult;
|
|
4155
4321
|
try {
|
|
4156
|
-
|
|
4322
|
+
await _dispatchMenuChoice(argv);
|
|
4157
4323
|
} catch (e) {
|
|
4158
4324
|
process.stderr.write(`\n ${accent('✗')} ${e?.message || String(e)}\n`);
|
|
4159
4325
|
}
|
|
4160
|
-
// Subcommand asked for full lazyclaw exit (currently only chat's
|
|
4161
|
-
// /exit). Break the launcher loop so the finally block tears
|
|
4162
|
-
// down stdin and the process ends naturally — without this, the
|
|
4163
|
-
// user has to /exit out of chat AND pick Quit from the menu to
|
|
4164
|
-
// actually leave.
|
|
4165
|
-
if (dispatchResult === 'LAZYCLAW_EXIT') return;
|
|
4166
4326
|
|
|
4167
4327
|
// Pause before re-drawing so the user can read the subcommand's
|
|
4168
4328
|
// output. `chat` is the special case: its REPL has already kept
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lazyclaw",
|
|
3
|
-
"version": "3.99.
|
|
3
|
+
"version": "3.99.15",
|
|
4
4
|
"description": "Lazy, elegant terminal CLI for chatting with Claude / OpenAI / Gemini / Ollama and orchestrating multi-step LLM workflows. Banner-on-launch, slash-command ghost autocomplete, persistent sessions, local HTTP gateway.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|
package/providers/registry.mjs
CHANGED
|
@@ -52,6 +52,164 @@ export const mockProvider = {
|
|
|
52
52
|
export { anthropicProvider, openaiProvider, ollamaProvider, geminiProvider, claudeCliProvider };
|
|
53
53
|
export { makeOpenAICompatProvider, fetchOpenAICompatModels };
|
|
54
54
|
|
|
55
|
+
// Built-in OpenAI-compatible vendors. Same wire format → one factory call
|
|
56
|
+
// each. The picker treats these like first-class providers so users don't
|
|
57
|
+
// have to walk through "+ Add a custom endpoint" for the popular ones.
|
|
58
|
+
//
|
|
59
|
+
// Each entry must define baseUrl + envKey (the env var the chat path
|
|
60
|
+
// consults when no api-key is configured) + suggestedModels (curated list
|
|
61
|
+
// shown before the user fetches the live /v1/models catalogue).
|
|
62
|
+
//
|
|
63
|
+
// Adding a new vendor: drop a row here. The PROVIDERS / PROVIDER_INFO loops
|
|
64
|
+
// below pick it up automatically.
|
|
65
|
+
export const OPENAI_COMPAT_BUILTINS = {
|
|
66
|
+
nim: {
|
|
67
|
+
label: 'NVIDIA NIM',
|
|
68
|
+
baseUrl: 'https://integrate.api.nvidia.com/v1',
|
|
69
|
+
envKey: 'NVIDIA_API_KEY',
|
|
70
|
+
altEnvKeys: ['NIM_API_KEY'],
|
|
71
|
+
keyPrefix: 'nvapi-',
|
|
72
|
+
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.',
|
|
73
|
+
defaultModel: 'meta/llama-3.1-405b-instruct',
|
|
74
|
+
suggestedModels: [
|
|
75
|
+
'meta/llama-3.1-405b-instruct',
|
|
76
|
+
'meta/llama-3.1-70b-instruct',
|
|
77
|
+
'meta/llama-3.1-8b-instruct',
|
|
78
|
+
'nvidia/llama-3.1-nemotron-70b-instruct',
|
|
79
|
+
'nvidia/nemotron-mini-4b-instruct',
|
|
80
|
+
'nvidia/llama-3.3-nemotron-super-49b-v1',
|
|
81
|
+
'mistralai/mistral-nemo-12b-instruct',
|
|
82
|
+
'mistralai/mixtral-8x22b-instruct-v0.1',
|
|
83
|
+
'microsoft/phi-3-medium-4k-instruct',
|
|
84
|
+
'deepseek-ai/deepseek-r1',
|
|
85
|
+
'qwen/qwen2.5-7b-instruct',
|
|
86
|
+
'qwen/qwen2.5-coder-32b-instruct',
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
openrouter: {
|
|
90
|
+
label: 'OpenRouter',
|
|
91
|
+
baseUrl: 'https://openrouter.ai/api/v1',
|
|
92
|
+
envKey: 'OPENROUTER_API_KEY',
|
|
93
|
+
keyPrefix: 'sk-or-',
|
|
94
|
+
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.',
|
|
95
|
+
defaultModel: 'anthropic/claude-3.5-sonnet',
|
|
96
|
+
headers: { 'http-referer': 'https://github.com/cmblir/lazyclaude', 'x-title': 'lazyclaw' },
|
|
97
|
+
suggestedModels: [
|
|
98
|
+
'anthropic/claude-3.5-sonnet',
|
|
99
|
+
'anthropic/claude-3-opus',
|
|
100
|
+
'openai/gpt-4o',
|
|
101
|
+
'openai/gpt-4o-mini',
|
|
102
|
+
'openai/o1-preview',
|
|
103
|
+
'meta-llama/llama-3.1-405b-instruct',
|
|
104
|
+
'meta-llama/llama-3.3-70b-instruct',
|
|
105
|
+
'google/gemini-2.0-flash-exp:free',
|
|
106
|
+
'google/gemini-pro-1.5',
|
|
107
|
+
'deepseek/deepseek-chat',
|
|
108
|
+
'deepseek/deepseek-r1',
|
|
109
|
+
'qwen/qwen-2.5-coder-32b-instruct',
|
|
110
|
+
'mistralai/mistral-large',
|
|
111
|
+
],
|
|
112
|
+
},
|
|
113
|
+
groq: {
|
|
114
|
+
label: 'Groq',
|
|
115
|
+
baseUrl: 'https://api.groq.com/openai/v1',
|
|
116
|
+
envKey: 'GROQ_API_KEY',
|
|
117
|
+
keyPrefix: 'gsk_',
|
|
118
|
+
docs: 'Groq LPU inference — fastest-token-per-second tier for Llama / Mixtral / Gemma. Auth: GROQ_API_KEY env var or in-app api-key.',
|
|
119
|
+
defaultModel: 'llama-3.3-70b-versatile',
|
|
120
|
+
suggestedModels: [
|
|
121
|
+
'llama-3.3-70b-versatile',
|
|
122
|
+
'llama-3.1-70b-versatile',
|
|
123
|
+
'llama-3.1-8b-instant',
|
|
124
|
+
'llama-3.2-90b-vision-preview',
|
|
125
|
+
'mixtral-8x7b-32768',
|
|
126
|
+
'gemma2-9b-it',
|
|
127
|
+
'qwen-2.5-coder-32b',
|
|
128
|
+
'qwen-2.5-32b',
|
|
129
|
+
'deepseek-r1-distill-llama-70b',
|
|
130
|
+
],
|
|
131
|
+
},
|
|
132
|
+
together: {
|
|
133
|
+
label: 'Together AI',
|
|
134
|
+
baseUrl: 'https://api.together.xyz/v1',
|
|
135
|
+
envKey: 'TOGETHER_API_KEY',
|
|
136
|
+
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.',
|
|
137
|
+
defaultModel: 'meta-llama/Llama-3.3-70B-Instruct-Turbo',
|
|
138
|
+
suggestedModels: [
|
|
139
|
+
'meta-llama/Llama-3.3-70B-Instruct-Turbo',
|
|
140
|
+
'meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo',
|
|
141
|
+
'meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo',
|
|
142
|
+
'meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo',
|
|
143
|
+
'mistralai/Mixtral-8x22B-Instruct-v0.1',
|
|
144
|
+
'mistralai/Mixtral-8x7B-Instruct-v0.1',
|
|
145
|
+
'Qwen/Qwen2.5-72B-Instruct-Turbo',
|
|
146
|
+
'Qwen/Qwen2.5-Coder-32B-Instruct',
|
|
147
|
+
'deepseek-ai/DeepSeek-V3',
|
|
148
|
+
'deepseek-ai/DeepSeek-R1',
|
|
149
|
+
],
|
|
150
|
+
},
|
|
151
|
+
xai: {
|
|
152
|
+
label: 'xAI (Grok)',
|
|
153
|
+
baseUrl: 'https://api.x.ai/v1',
|
|
154
|
+
envKey: 'XAI_API_KEY',
|
|
155
|
+
altEnvKeys: ['GROK_API_KEY'],
|
|
156
|
+
keyPrefix: 'xai-',
|
|
157
|
+
docs: 'xAI Grok models. Auth: XAI_API_KEY env var or in-app api-key.',
|
|
158
|
+
defaultModel: 'grok-2-latest',
|
|
159
|
+
suggestedModels: [
|
|
160
|
+
'grok-2-latest',
|
|
161
|
+
'grok-2-1212',
|
|
162
|
+
'grok-2-vision-1212',
|
|
163
|
+
'grok-beta',
|
|
164
|
+
'grok-vision-beta',
|
|
165
|
+
],
|
|
166
|
+
},
|
|
167
|
+
deepseek: {
|
|
168
|
+
label: 'DeepSeek',
|
|
169
|
+
baseUrl: 'https://api.deepseek.com/v1',
|
|
170
|
+
envKey: 'DEEPSEEK_API_KEY',
|
|
171
|
+
keyPrefix: 'sk-',
|
|
172
|
+
docs: 'DeepSeek (deepseek-chat / deepseek-reasoner). Auth: DEEPSEEK_API_KEY env var or in-app api-key.',
|
|
173
|
+
defaultModel: 'deepseek-chat',
|
|
174
|
+
suggestedModels: [
|
|
175
|
+
'deepseek-chat',
|
|
176
|
+
'deepseek-reasoner',
|
|
177
|
+
'deepseek-coder',
|
|
178
|
+
],
|
|
179
|
+
},
|
|
180
|
+
mistral: {
|
|
181
|
+
label: 'Mistral La Plateforme',
|
|
182
|
+
baseUrl: 'https://api.mistral.ai/v1',
|
|
183
|
+
envKey: 'MISTRAL_API_KEY',
|
|
184
|
+
docs: 'Mistral La Plateforme (mistral-large, codestral, ministral, pixtral). Auth: MISTRAL_API_KEY env var or in-app api-key.',
|
|
185
|
+
defaultModel: 'mistral-large-latest',
|
|
186
|
+
suggestedModels: [
|
|
187
|
+
'mistral-large-latest',
|
|
188
|
+
'mistral-small-latest',
|
|
189
|
+
'codestral-latest',
|
|
190
|
+
'ministral-8b-latest',
|
|
191
|
+
'ministral-3b-latest',
|
|
192
|
+
'pixtral-large-latest',
|
|
193
|
+
'open-mistral-nemo',
|
|
194
|
+
],
|
|
195
|
+
},
|
|
196
|
+
fireworks: {
|
|
197
|
+
label: 'Fireworks AI',
|
|
198
|
+
baseUrl: 'https://api.fireworks.ai/inference/v1',
|
|
199
|
+
envKey: 'FIREWORKS_API_KEY',
|
|
200
|
+
docs: 'Fireworks AI hosted models. Auth: FIREWORKS_API_KEY env var or in-app api-key.',
|
|
201
|
+
defaultModel: 'accounts/fireworks/models/llama-v3p3-70b-instruct',
|
|
202
|
+
suggestedModels: [
|
|
203
|
+
'accounts/fireworks/models/llama-v3p3-70b-instruct',
|
|
204
|
+
'accounts/fireworks/models/llama-v3p1-405b-instruct',
|
|
205
|
+
'accounts/fireworks/models/qwen2p5-coder-32b-instruct',
|
|
206
|
+
'accounts/fireworks/models/deepseek-r1',
|
|
207
|
+
'accounts/fireworks/models/deepseek-v3',
|
|
208
|
+
'accounts/fireworks/models/mixtral-8x22b-instruct',
|
|
209
|
+
],
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
|
|
55
213
|
// Insertion order is the picker order. The list goes first-to-last in
|
|
56
214
|
// rough "user-familiar / popular" order so a first-time onboard lands
|
|
57
215
|
// the cursor on a vendor most users recognise. v3.99.5 reordered per
|
|
@@ -63,11 +221,34 @@ export const PROVIDERS = {
|
|
|
63
221
|
// Tier 2 — Claude. CLI variant first because it's keyless.
|
|
64
222
|
'claude-cli': claudeCliProvider,
|
|
65
223
|
anthropic: anthropicProvider,
|
|
66
|
-
// Tier 3 —
|
|
224
|
+
// Tier 3 — popular OpenAI-compatible aggregators / hosted catalogues.
|
|
225
|
+
// Inserted by the loop below from OPENAI_COMPAT_BUILTINS so the order
|
|
226
|
+
// here mirrors that object's insertion order.
|
|
227
|
+
// Tier 4 — local + dev/test.
|
|
67
228
|
ollama: ollamaProvider,
|
|
68
229
|
mock: mockProvider,
|
|
69
230
|
};
|
|
70
231
|
|
|
232
|
+
// Wire each OpenAI-compat builtin into PROVIDERS as a callable provider.
|
|
233
|
+
// Insertion is between Tier 2 (anthropic) and Tier 4 (ollama) by reordering
|
|
234
|
+
// the keys after the loop runs — JS objects honour insertion order and
|
|
235
|
+
// cmdLauncher's families helper relies on that for the picker.
|
|
236
|
+
{
|
|
237
|
+
const local = { ollama: PROVIDERS.ollama, mock: PROVIDERS.mock };
|
|
238
|
+
delete PROVIDERS.ollama;
|
|
239
|
+
delete PROVIDERS.mock;
|
|
240
|
+
for (const [name, def] of Object.entries(OPENAI_COMPAT_BUILTINS)) {
|
|
241
|
+
PROVIDERS[name] = makeOpenAICompatProvider({
|
|
242
|
+
name,
|
|
243
|
+
baseUrl: def.baseUrl,
|
|
244
|
+
defaultModel: def.defaultModel,
|
|
245
|
+
headers: def.headers,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
PROVIDERS.ollama = local.ollama;
|
|
249
|
+
PROVIDERS.mock = local.mock;
|
|
250
|
+
}
|
|
251
|
+
|
|
71
252
|
// Static metadata for `lazyclaw providers list/info`. Kept next to PROVIDERS
|
|
72
253
|
// so adding a provider in one place can't drift from the list shown to users.
|
|
73
254
|
export const PROVIDER_INFO = {
|
|
@@ -167,6 +348,44 @@ export const PROVIDER_INFO = {
|
|
|
167
348
|
},
|
|
168
349
|
};
|
|
169
350
|
|
|
351
|
+
// Mirror the OpenAI-compat builtins into PROVIDER_INFO so picker / docs /
|
|
352
|
+
// `lazyclaw providers info` see them with the same shape as the hand-written
|
|
353
|
+
// entries above.
|
|
354
|
+
for (const [name, def] of Object.entries(OPENAI_COMPAT_BUILTINS)) {
|
|
355
|
+
PROVIDER_INFO[name] = {
|
|
356
|
+
name,
|
|
357
|
+
label: def.label,
|
|
358
|
+
requiresApiKey: true,
|
|
359
|
+
keyPrefix: def.keyPrefix,
|
|
360
|
+
envKey: def.envKey,
|
|
361
|
+
altEnvKeys: Array.isArray(def.altEnvKeys) ? def.altEnvKeys.slice() : [],
|
|
362
|
+
docs: def.docs,
|
|
363
|
+
endpoint: `${def.baseUrl}/chat/completions`,
|
|
364
|
+
defaultModel: def.defaultModel,
|
|
365
|
+
suggestedModels: Array.isArray(def.suggestedModels) ? def.suggestedModels.slice() : [],
|
|
366
|
+
builtin: true,
|
|
367
|
+
builtinOpenAICompat: true,
|
|
368
|
+
baseUrl: def.baseUrl,
|
|
369
|
+
headers: def.headers,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Resolve an api-key for a built-in OpenAI-compatible provider from the
|
|
375
|
+
* environment, scanning {envKey} then any {altEnvKeys}. Returns '' when
|
|
376
|
+
* no env var is set so the caller can fall through to its config-based
|
|
377
|
+
* lookup chain.
|
|
378
|
+
*/
|
|
379
|
+
export function resolveBuiltinEnvKey(provider) {
|
|
380
|
+
const meta = PROVIDER_INFO[provider];
|
|
381
|
+
if (!meta || !meta.builtinOpenAICompat) return '';
|
|
382
|
+
const candidates = [meta.envKey, ...(meta.altEnvKeys || [])].filter(Boolean);
|
|
383
|
+
for (const k of candidates) {
|
|
384
|
+
if (process.env[k]) return process.env[k];
|
|
385
|
+
}
|
|
386
|
+
return '';
|
|
387
|
+
}
|
|
388
|
+
|
|
170
389
|
/**
|
|
171
390
|
* Split a unified "provider/model" string (OpenClaw style:
|
|
172
391
|
* "anthropic/claude-opus-4-7"). Also accepts a bare model id and returns
|
|
@@ -194,13 +413,27 @@ export function parseProviderModel(s) {
|
|
|
194
413
|
* @param {string|undefined|null} key
|
|
195
414
|
* @returns {string}
|
|
196
415
|
*/
|
|
197
|
-
// Reserved provider names —
|
|
198
|
-
//
|
|
416
|
+
// Reserved provider names — names whose factory is bespoke (not the
|
|
417
|
+
// generic OpenAI-compat one) so a custom registration of the same name
|
|
418
|
+
// would silently break the wire format. The OpenAI-compat builtins are
|
|
419
|
+
// deliberately NOT listed: a user can register `nim` / `openrouter` /
|
|
420
|
+
// etc. as a custom entry to override the baseUrl / api-key / headers,
|
|
421
|
+
// because both the built-in and the custom go through
|
|
422
|
+
// `makeOpenAICompatProvider` — overriding is well-defined.
|
|
199
423
|
const RESERVED_PROVIDER_NAMES = new Set([
|
|
200
424
|
'mock', 'claude-cli', 'anthropic', 'openai', 'gemini', 'ollama',
|
|
201
425
|
'__add_custom__', '__custom_model__', '__fetch_models__',
|
|
202
426
|
]);
|
|
203
427
|
|
|
428
|
+
/**
|
|
429
|
+
* Whether the supplied name belongs to one of the OpenAI-compatible
|
|
430
|
+
* builtins. Used by the custom-add interactive flow so it can warn the
|
|
431
|
+
* user that their custom entry will shadow the built-in registration.
|
|
432
|
+
*/
|
|
433
|
+
export function isBuiltinOpenAICompatName(name) {
|
|
434
|
+
return Object.prototype.hasOwnProperty.call(OPENAI_COMPAT_BUILTINS, String(name || '').trim().toLowerCase());
|
|
435
|
+
}
|
|
436
|
+
|
|
204
437
|
/**
|
|
205
438
|
* Validate a custom provider name. Allowed: lowercase alnum + dash + dot.
|
|
206
439
|
* Returns the trimmed name on success; throws on collision / bad format.
|