loom-agent 1.3.3 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,116 @@
1
- async function updateCheck() {
2
- if (process.env.LOOM_NO_UPDATE_CHECK) return;
3
- return;
4
- }
5
-
6
- module.exports = { updateCheck };
1
+ // Startup update check — a quiet, non-blocking "is there a newer version of
2
+ // loom-agent on npm?" probe that runs once per day and prints at most one line
3
+ // to stderr before the TUI/REPL takes over the terminal.
4
+ //
5
+ // Design constraints:
6
+ // - Non-blocking: runs concurrently with `main()`, never gates startup, and
7
+ // any failure (offline, registry down, malformed body) is swallowed — this
8
+ // must never break the app or add latency to first render.
9
+ // - Throttled: one network probe per 24h, tracked by a stamp file so it
10
+ // survives across runs.
11
+ // - Silent when up-to-date; silent when disabled via LOOM_NO_UPDATE_CHECK.
12
+ // Only prints when a strictly newer version is actually available.
13
+ // - Compare is a plain semver-ish tuple compare (major.minor.patch), not a
14
+ // library — enough for the "newer than installed?" question without a dep.
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+ const os = require('os');
19
+
20
+ const PKG = require('../../package.json');
21
+ const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24h between probes
22
+ const FETCH_TIMEOUT_MS = 4000; // fail fast — this is best-effort only
23
+
24
+ function stampFile() {
25
+ const dir = process.env.LOOM_CONFIG_DIR || path.join(os.homedir(), '.loom');
26
+ return path.join(dir, 'update-check.json');
27
+ }
28
+
29
+ // Extract a comparable version tuple. Returns null for anything unparseable.
30
+ function parseVersion(v) {
31
+ if (typeof v !== 'string') return null;
32
+ const m = v.trim().match(/^v?(\d+)\.(\d+)\.(\d+)/);
33
+ if (!m) return null;
34
+ return [Number(m[1]), Number(m[2]), Number(m[3])];
35
+ }
36
+
37
+ // True when `latest` is strictly newer than `current`. Pre-release / build
38
+ // suffixes are ignored (treated as the base triple) — good enough here.
39
+ function isNewer(latest, current) {
40
+ const a = parseVersion(latest);
41
+ const b = parseVersion(current);
42
+ if (!a || !b) return false;
43
+ for (let i = 0; i < 3; i++) {
44
+ if (a[i] > b[i]) return true;
45
+ if (a[i] < b[i]) return false;
46
+ }
47
+ return false;
48
+ }
49
+
50
+ // Read the last-check timestamp; returns 0 if absent/corrupt (so the very
51
+ // first run after install always checks).
52
+ function lastCheckAt() {
53
+ try {
54
+ const raw = fs.readFileSync(stampFile(), 'utf8');
55
+ const n = Number(JSON.parse(raw).at);
56
+ return Number.isFinite(n) ? n : 0;
57
+ } catch {
58
+ return 0;
59
+ }
60
+ }
61
+
62
+ function markChecked() {
63
+ try {
64
+ const file = stampFile();
65
+ fs.mkdirSync(path.dirname(file), { recursive: true });
66
+ fs.writeFileSync(file, JSON.stringify({ at: Date.now(), checked: PKG.version }));
67
+ } catch {
68
+ // Throttle stamp is best-effort — a failure just means we probe again
69
+ // next run, never breaks startup.
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Probe npm for the latest `loom-agent` version and, if it's newer than the
75
+ * installed one, print a one-line upgrade hint to stderr. Throttled to one
76
+ * probe per 24h. Never throws, never blocks, and returns the newer version
77
+ * string (or null) so tests can assert the decision without inspecting
78
+ * stderr.
79
+ *
80
+ * @returns {Promise<string|null>} latest version when an upgrade exists, else null
81
+ */
82
+ async function updateCheck() {
83
+ if (process.env.LOOM_NO_UPDATE_CHECK) return null;
84
+
85
+ const now = Date.now();
86
+ if (now - lastCheckAt() < CHECK_INTERVAL_MS) return null;
87
+
88
+ // Claim the slot BEFORE the fetch: even if the network call fails we don't
89
+ // want to retry on every startup when offline.
90
+ markChecked();
91
+
92
+ try {
93
+ const ctrl = new AbortController();
94
+ const timer = setTimeout(() => ctrl.abort(), FETCH_TIMEOUT_MS);
95
+ let res;
96
+ try {
97
+ res = await fetch('https://registry.npmjs.org/loom-agent/latest', { signal: ctrl.signal });
98
+ } finally {
99
+ clearTimeout(timer);
100
+ }
101
+ if (!res || !res.ok) return null;
102
+ const body = /** @type {{version?: unknown}} */ (await res.json());
103
+ const latest = typeof body?.version === 'string' ? body.version : null;
104
+ if (!isNewer(latest, PKG.version)) return null;
105
+
106
+ process.stderr.write(
107
+ `\n[loom] New version available: ${PKG.version} → ${latest}\n` +
108
+ `[loom] Upgrade with: npm install -g ${PKG.name}@latest\n\n`
109
+ );
110
+ return latest;
111
+ } catch {
112
+ return null;
113
+ }
114
+ }
115
+
116
+ module.exports = { updateCheck, isNewer, parseVersion };
@@ -0,0 +1,105 @@
1
+ // Hosting / cloud-service connector presets — platforms you "connect" your
2
+ // project to rather than dev-tool servers. Surfaced under /connectors (and
3
+ // `loom add connector`), not /mcp. Lives in the core layer so the CLI
4
+ // (`loom add connector <preset> -e KEY=V`) and the TUI picker share one
5
+ // catalog; the TUI re-exports this list from src/tui/mcp-presets.ts.
6
+ //
7
+ // Each preset: id, label, package (npx-run) or command (full override, e.g.
8
+ // docker / npx mcp-remote), description, args (may reference $KEY env
9
+ // placeholders resolved at add time), env defaults, prompts (guided key
10
+ // entry + the required-key set for CLI expansion), and an optional
11
+ // optionalArgsPrompt (non-secret trailing arg, e.g. --project-ref).
12
+
13
+ /**
14
+ * @typedef {Object} ConnectorPrompt
15
+ * @property {string} key
16
+ * @property {string} label
17
+ * @property {boolean} [mask]
18
+ */
19
+
20
+ /**
21
+ * @typedef {Object} OptionalArgsPrompt
22
+ * @property {string|null} flag
23
+ * @property {string} label
24
+ */
25
+
26
+ /**
27
+ * @typedef {Object} ConnectorPreset
28
+ * @property {string} id
29
+ * @property {string} label
30
+ * @property {string} [package]
31
+ * @property {string} [command]
32
+ * @property {string} description
33
+ * @property {string[]} args
34
+ * @property {Record<string, string>} env
35
+ * @property {ConnectorPrompt[]} prompts
36
+ * @property {OptionalArgsPrompt} [optionalArgsPrompt]
37
+ */
38
+
39
+ /** @type {ConnectorPreset[]} */
40
+ const CONNECTOR_PRESETS = [
41
+ {
42
+ id: 'supabase',
43
+ label: 'Supabase',
44
+ package: '@supabase/mcp-server-supabase',
45
+ description: 'Query schema, run SQL, manage Supabase projects',
46
+ args: ['--access-token', '$SUPABASE_ACCESS_TOKEN'],
47
+ env: { SUPABASE_ACCESS_TOKEN: '' },
48
+ prompts: [
49
+ { key: 'SUPABASE_ACCESS_TOKEN', label: 'Personal access token (supabase.com/dashboard/account/tokens)', mask: true },
50
+ ],
51
+ optionalArgsPrompt: { flag: '--project-ref', label: 'Project ref (optional, e.g. abcdefghijklmnop) — Enter to skip' },
52
+ },
53
+ {
54
+ id: 'nextjs',
55
+ label: 'Next.js',
56
+ package: 'nextjs-mcp-server',
57
+ description: 'Next.js project introspection (routes, pages, config)',
58
+ args: [],
59
+ env: {},
60
+ prompts: [],
61
+ },
62
+ {
63
+ id: 'railway',
64
+ label: 'Railway',
65
+ package: '@railway/mcp-server',
66
+ description: 'Railway deploys, services, variables',
67
+ args: [],
68
+ env: { RAILWAY_API_TOKEN: '' },
69
+ prompts: [
70
+ { key: 'RAILWAY_API_TOKEN', label: 'API token (railway.app/account/tokens)', mask: true },
71
+ ],
72
+ },
73
+ {
74
+ id: 'vercel',
75
+ label: 'Vercel',
76
+ command: 'npx',
77
+ args: ['-y', 'mcp-remote', 'https://mcp.vercel.com'],
78
+ description: 'Vercel deployments and projects (remote MCP endpoint, OAuth)',
79
+ env: {},
80
+ prompts: [],
81
+ },
82
+ {
83
+ id: 'netlify',
84
+ label: 'Netlify',
85
+ command: 'npx',
86
+ args: ['-y', 'mcp-remote', 'https://api.netlify.com/mcp'],
87
+ description: 'Netlify sites, deploys, and environment vars (remote MCP endpoint, OAuth)',
88
+ env: {},
89
+ prompts: [],
90
+ },
91
+ {
92
+ id: 'cloudflare',
93
+ label: 'Cloudflare',
94
+ package: '@cloudflare/mcp-server-cloudflare',
95
+ description: 'Workers, DNS, and account management',
96
+ args: [],
97
+ env: { CLOUDFLARE_API_TOKEN: '', CLOUDFLARE_ACCOUNT_ID: '' },
98
+ prompts: [
99
+ { key: 'CLOUDFLARE_API_TOKEN', label: 'API token (dash.cloudflare.com/profile/api-tokens)', mask: true },
100
+ { key: 'CLOUDFLARE_ACCOUNT_ID', label: 'Account ID (dash.cloudflare.com — right sidebar)', mask: false },
101
+ ],
102
+ },
103
+ ];
104
+
105
+ module.exports = { CONNECTOR_PRESETS };
@@ -54,9 +54,17 @@ function buildBody(messages, options) {
54
54
  // resumed/hand-edited sessions rather than failing the whole turn.
55
55
  const tcid = m.toolCallId || m.tool_call_id;
56
56
  if (tcid) {
57
+ // is_error marks a failed tool call so the model treats the string
58
+ // content as an error report, not a successful result (parity with
59
+ // the OpenAI-compat is_error/tool-result semantics).
57
60
  out.push({
58
61
  role: 'user',
59
- content: [{ type: 'tool_result', tool_use_id: tcid, content: String(m.content) }],
62
+ content: [{
63
+ type: 'tool_result',
64
+ tool_use_id: tcid,
65
+ content: String(m.content),
66
+ ...(m.isError ? { is_error: true } : {}),
67
+ }],
60
68
  });
61
69
  }
62
70
  continue;
@@ -156,29 +164,69 @@ function normalizeBlocks(contentBlocks) {
156
164
  return { content: textParts.join(''), toolCalls };
157
165
  }
158
166
 
167
+ // Retry-After header (seconds) or rate-limit reset timestamps → wait ms.
168
+ function retryAfterMs(resp) {
169
+ const ra = resp && resp.headers && resp.headers.get('retry-after');
170
+ if (ra) {
171
+ const secs = Number(ra);
172
+ if (Number.isFinite(secs) && secs >= 0) return Math.min(secs * 1000, 60000);
173
+ }
174
+ return null;
175
+ }
176
+
177
+ // Retryable: rate limits (429) and transient server errors (5xx/529).
178
+ function isRetryableStatus(status) {
179
+ return status === 429 || status === 529 || (status >= 500 && status < 600);
180
+ }
181
+
159
182
  async function post(body, options) {
160
183
  const key = getKey();
161
184
  if (!key) throw new Error("Anthropic API key not set. Use /connect anthropic or set ANTHROPIC_API_KEY.");
162
185
  const apiUrl = getApiUrl();
163
- const resp = await fetch(apiUrl, {
164
- method: 'POST',
165
- headers: {
166
- 'Content-Type': 'application/json',
167
- 'x-api-key': key,
168
- 'anthropic-version': '2023-06-01',
169
- },
170
- body: JSON.stringify(body),
171
- signal: options.signal,
172
- });
173
- if (!resp.ok) {
174
- /** @type {any} */
175
- const err = await resp.json().catch(() => ({}));
176
- const detail = err.error?.message || resp.statusText;
177
- if (resp.status === 401) throw new Error("Anthropic 401 Unauthorized: the API key is invalid or expired. Run /connect anthropic and paste a new key.");
178
- if (resp.status === 403) throw new Error("Anthropic 403 Forbidden: the API key is not authorized for this model. Check the model name and your account access.");
179
- throw new Error(`Anthropic API error ${resp.status}: ${detail}`);
186
+ const maxTries = 4;
187
+ let resp = null;
188
+ for (let attempt = 0; attempt < maxTries; attempt++) {
189
+ resp = await fetch(apiUrl, {
190
+ method: 'POST',
191
+ headers: {
192
+ 'Content-Type': 'application/json',
193
+ 'x-api-key': key,
194
+ 'anthropic-version': '2023-06-01',
195
+ },
196
+ body: JSON.stringify(body),
197
+ signal: options.signal,
198
+ });
199
+ if (resp.ok) return resp;
200
+ if (!isRetryableStatus(resp.status) || attempt === maxTries - 1) break;
201
+ // Honor the provider's Retry-After (seconds); else exponential backoff
202
+ // with jitter so parallel retries don't stampede.
203
+ const waitMs = retryAfterMs(resp) ?? Math.min(1000 * Math.pow(2, attempt), 30000);
204
+ const jitter = Math.floor(Math.random() * 250);
205
+ await new Promise((r) => setTimeout(r, waitMs + jitter));
206
+ if (options.signal && options.signal.aborted) {
207
+ const abortErr = new Error('Request aborted');
208
+ abortErr.name = 'AbortError';
209
+ throw abortErr;
210
+ }
180
211
  }
181
- return resp;
212
+ if (!resp) throw new Error('Anthropic API error: no response');
213
+ /** @type {any} */
214
+ const err = await resp.json().catch(() => ({}));
215
+ const detail = err.error?.message || resp.statusText;
216
+ if (resp.status === 401) throw new Error("Anthropic 401 Unauthorized: the API key is invalid or expired. Run /connect anthropic and paste a new key.");
217
+ if (resp.status === 403) throw new Error("Anthropic 403 Forbidden: the API key is not authorized for this model. Check the model name and your account access.");
218
+ const e = new Error(`Anthropic API error ${resp.status}: ${detail}`);
219
+ /** @type {any} */ (e).status = resp.status;
220
+ /** @type {any} */ (e).retryAfterMs = retryAfterMs(resp);
221
+ throw e;
222
+ }
223
+
224
+ // Anthropic stop_reason → the finishReason vocabulary the session loop knows
225
+ // ('length' drives truncation auto-continue; 'tool_use'/'end_turn' are no-ops).
226
+ function mapStopReason(stopReason) {
227
+ if (stopReason === 'max_tokens') return 'length';
228
+ if (stopReason === 'tool_use') return 'tool_calls';
229
+ return stopReason || undefined;
182
230
  }
183
231
 
184
232
  async function chat(messages, options = {}) {
@@ -186,7 +234,11 @@ async function chat(messages, options = {}) {
186
234
  const resp = await post(body, options);
187
235
  /** @type {any} */
188
236
  const data = await resp.json();
189
- return { ...normalizeBlocks(data.content || []), usage: data.usage };
237
+ return {
238
+ ...normalizeBlocks(data.content || []),
239
+ usage: data.usage,
240
+ finishReason: mapStopReason(data.stop_reason),
241
+ };
190
242
  }
191
243
 
192
244
  async function stream(messages, options = {}, onDelta, onReasoning) {
@@ -198,6 +250,8 @@ async function stream(messages, options = {}, onDelta, onReasoning) {
198
250
  let content = '';
199
251
  let reasoning = '';
200
252
  let usage = null;
253
+ let finishReason;
254
+ let sawEnd = false;
201
255
  const toolAcc = new Map();
202
256
 
203
257
  for await (const chunk of resp.body) {
@@ -210,7 +264,25 @@ async function stream(messages, options = {}, onDelta, onReasoning) {
210
264
  if (raw === '[DONE]') continue;
211
265
  let evt;
212
266
  try { evt = JSON.parse(raw); } catch { continue; }
213
- if (evt.type === 'message_delta' && evt.usage) usage = evt.usage;
267
+ // Mid-stream error events (content filter, overloaded after start)
268
+ // previously vanished — the turn dead-ended with empty content. Throw
269
+ // so the session retry/error path handles it.
270
+ if (evt.type === 'error') {
271
+ const emsg = evt.error?.message || 'unknown stream error';
272
+ const e = new Error(`Anthropic stream error: ${emsg}`);
273
+ /** @type {any} */ (e).status = evt.error?.type === 'overloaded_error' ? 529 : undefined;
274
+ throw e;
275
+ }
276
+ if (evt.type === 'message_start' && evt.message?.usage) {
277
+ // message_start carries input_tokens (prompt) — without it, a turn
278
+ // that ends in message_delta only undercounts input to zero.
279
+ usage = { ...evt.message.usage };
280
+ }
281
+ if (evt.type === 'message_delta') {
282
+ if (evt.usage) usage = { ...(usage || {}), ...evt.usage };
283
+ if (evt.delta && evt.delta.stop_reason) finishReason = mapStopReason(evt.delta.stop_reason);
284
+ sawEnd = true;
285
+ }
214
286
  if (evt.type === 'content_block_start' && evt.content_block?.type === 'tool_use') {
215
287
  toolAcc.set(evt.index, { id: evt.content_block.id, name: evt.content_block.name, input: '' });
216
288
  } else if (evt.type === 'content_block_delta') {
@@ -229,6 +301,14 @@ async function stream(messages, options = {}, onDelta, onReasoning) {
229
301
  }
230
302
  }
231
303
 
304
+ // A stream that never delivered message_delta AND produced nothing was
305
+ // truncated (network reset mid-body). Surface it as an error instead of a
306
+ // silent empty reply. Partial content with no trailing message_delta is
307
+ // kept (the session can continue from it).
308
+ if (!sawEnd && !usage && !content && !toolAcc.size) {
309
+ throw new Error('Anthropic stream truncated: connection ended before message_delta');
310
+ }
311
+
232
312
  const toolCalls = [];
233
313
  for (const acc of toolAcc.values()) {
234
314
  let input = {};
@@ -236,13 +316,30 @@ async function stream(messages, options = {}, onDelta, onReasoning) {
236
316
  toolCalls.push({ id: acc.id, name: acc.name, input });
237
317
  }
238
318
 
239
- return { content, reasoning, toolCalls, usage };
319
+ return { content, reasoning, toolCalls, usage, finishReason };
240
320
  }
241
321
 
322
+ // Current Anthropic lineup, verified against the models.dev registry
323
+ // (2026-10-17). Dated snapshots kept for back-compat with saved configs.
242
324
  const models = [
243
- { id: 'claude-sonnet-4-20250514', name: 'Claude Sonnet 4', provider: 'anthropic', context: 200000, priceIn: 3, priceOut: 15, tags: ['reasoning'] },
244
- { id: 'claude-opus-4-20250514', name: 'Claude Opus 4', provider: 'anthropic', context: 200000, priceIn: 15, priceOut: 75, tags: ['reasoning'] },
245
- { id: 'claude-3-5-haiku-20241022', name: 'Claude 3.5 Haiku', provider: 'anthropic', context: 200000, priceIn: 0.8, priceOut: 4 },
325
+ // ── Opus ──
326
+ { id: 'claude-opus-5-5', name: 'Claude Opus 5.5', provider: 'anthropic', context: 1000000, priceIn: 4, priceOut: 20, tags: ['frontier', 'reasoning'] },
327
+ { id: 'claude-opus-5', name: 'Claude Opus 5', provider: 'anthropic', context: 1000000, priceIn: 5, priceOut: 25, tags: ['frontier', 'reasoning'] },
328
+ { id: 'claude-opus-4-8', name: 'Claude Opus 4.8', provider: 'anthropic', context: 1000000, priceIn: 5, priceOut: 25, tags: ['frontier', 'reasoning'] },
329
+ { id: 'claude-opus-4-5', name: 'Claude Opus 4.5', provider: 'anthropic', context: 200000, priceIn: 5, priceOut: 25, tags: ['frontier', 'reasoning'] },
330
+ { id: 'claude-fable-5-1', name: 'Claude Fable 5.1', provider: 'anthropic', context: 1000000, priceIn: 10, priceOut: 50, tags: ['frontier', 'reasoning'] },
331
+ // ── Sonnet ──
332
+ { id: 'claude-sonnet-5-5', name: 'Claude Sonnet 5.5', provider: 'anthropic', context: 1000000, priceIn: 2, priceOut: 10, tags: ['reasoning'] },
333
+ { id: 'claude-sonnet-5', name: 'Claude Sonnet 5', provider: 'anthropic', context: 1000000, priceIn: 2, priceOut: 10, tags: ['reasoning'] },
334
+ { id: 'claude-sonnet-4-6', name: 'Claude Sonnet 4.6', provider: 'anthropic', context: 1000000, priceIn: 3, priceOut: 15, tags: ['reasoning'] },
335
+ { id: 'claude-sonnet-4-5', name: 'Claude Sonnet 4.5', provider: 'anthropic', context: 200000, priceIn: 3, priceOut: 15, tags: ['reasoning'] },
336
+ // ── Haiku ──
337
+ { id: 'claude-haiku-5-5', name: 'Claude Haiku 5.5', provider: 'anthropic', context: 1000000, priceIn: 0.1, priceOut: 0.5, tags: ['fast', 'cheap', 'reasoning'] },
338
+ { id: 'claude-haiku-4-5', name: 'Claude Haiku 4.5', provider: 'anthropic', context: 200000, priceIn: 1, priceOut: 5, tags: ['fast', 'reasoning'] },
339
+ // ── Legacy snapshots ──
340
+ { id: 'claude-sonnet-4-20250514', name: 'Claude Sonnet 4', provider: 'anthropic', context: 200000, priceIn: 3, priceOut: 15, tags: ['reasoning', 'legacy'] },
341
+ { id: 'claude-opus-4-20250514', name: 'Claude Opus 4', provider: 'anthropic', context: 200000, priceIn: 15, priceOut: 75, tags: ['reasoning', 'legacy'] },
342
+ { id: 'claude-3-5-haiku-20241022', name: 'Claude 3.5 Haiku', provider: 'anthropic', context: 200000, priceIn: 0.8, priceOut: 4, tags: ['legacy'] },
246
343
  ];
247
344
 
248
345
  module.exports = { chat, stream, models, buildBody, normalizeBlocks };
@@ -12,17 +12,25 @@ const provider = {
12
12
  envKeyHint: 'GOOGLE',
13
13
  }),
14
14
  models: [
15
- // ── Frontier ──
16
- { id: 'gemini-2.5-pro', name: 'Gemini 2.5 Pro', provider: 'google', tags: ['frontier'], context: 1000000, priceIn: 1.25, priceOut: 10 },
17
- { id: 'gemini-2.5-flash', name: 'Gemini 2.5 Flash', provider: 'google', tags: ['fast'], context: 1000000, priceIn: 0.3, priceOut: 2.5 },
18
- // ── Flash ──
19
- { id: 'gemini-2.0-flash', name: 'Gemini 2.0 Flash', provider: 'google', tags: [], context: 1000000, priceIn: 0.1, priceOut: 0.4 },
20
- { id: 'gemini-2.0-flash-lite', name: 'Gemini 2.0 Flash Lite', provider: 'google', tags: ['free', 'fast'], context: 1000000, priceIn: 0.075, priceOut: 0.3 },
21
- // ── Legacy ──
22
- { id: 'gemini-1.5-flash', name: 'Gemini 1.5 Flash', provider: 'google', tags: [], context: 1000000, priceIn: 0.075, priceOut: 0.3 },
23
- { id: 'gemini-1.5-pro', name: 'Gemini 1.5 Pro', provider: 'google', tags: [], context: 2000000, priceIn: 1.25, priceOut: 5 },
24
- // ── Gemma (Local / On-device) ──
25
- { id: 'gemma-4-31b-it', name: 'Gemma 4 31B IT', provider: 'google', tags: ['small'], context: 32768, priceIn: 0.2, priceOut: 0.8 },
15
+ // ── Gemini 4 / 3.x (current) ──
16
+ { id: 'gemini-4-argon', name: 'Gemini 4 Argon', provider: 'google', tags: ['frontier', 'reasoning'], context: 1048576, priceIn: 2, priceOut: 10 },
17
+ { id: 'gemini-3.1-pro-preview', name: 'Gemini 3.1 Pro Preview', provider: 'google', tags: ['frontier', 'reasoning'], context: 1048576, priceIn: 2, priceOut: 12 },
18
+ { id: 'gemini-3.8-flash', name: 'Gemini 3.8 Flash', provider: 'google', tags: ['fast', 'reasoning'], context: 1048576, priceIn: 0.75, priceOut: 3.75 },
19
+ { id: 'gemini-3.7-flash', name: 'Gemini 3.7 Flash', provider: 'google', tags: ['fast', 'reasoning'], context: 1048576, priceIn: 0.75, priceOut: 3.75 },
20
+ { id: 'gemini-3.6-flash', name: 'Gemini 3.6 Flash', provider: 'google', tags: ['fast', 'reasoning'], context: 1048576, priceIn: 0.75, priceOut: 3.75 },
21
+ { id: 'gemini-3.5-flash', name: 'Gemini 3.5 Flash', provider: 'google', tags: ['reasoning'], context: 1048576, priceIn: 1.5, priceOut: 9 },
22
+ { id: 'gemini-3.5-flash-lite', name: 'Gemini 3.5 Flash Lite', provider: 'google', tags: ['fast', 'cheap', 'reasoning'], context: 1048576, priceIn: 0.3, priceOut: 2.5 },
23
+ { id: 'gemini-3.1-flash-lite', name: 'Gemini 3.1 Flash Lite', provider: 'google', tags: ['fast', 'cheap', 'reasoning'], context: 1048576, priceIn: 0.25, priceOut: 1.5 },
24
+ // ── Aliases ──
25
+ { id: 'gemini-flash-latest', name: 'Gemini Flash (Latest)', provider: 'google', tags: ['fast', 'reasoning'], context: 1048576, priceIn: 0.75, priceOut: 3.75 },
26
+ { id: 'gemini-flash-lite-latest', name: 'Gemini Flash-Lite (Latest)', provider: 'google', tags: ['fast', 'cheap', 'reasoning'], context: 1048576, priceIn: 0.3, priceOut: 2.5 },
27
+ // ── Gemini 2.5 (stable) ──
28
+ { id: 'gemini-2.5-pro', name: 'Gemini 2.5 Pro', provider: 'google', tags: ['frontier'], context: 1048576, priceIn: 1.25, priceOut: 10 },
29
+ { id: 'gemini-2.5-flash', name: 'Gemini 2.5 Flash', provider: 'google', tags: ['fast'], context: 1048576, priceIn: 0.3, priceOut: 2.5 },
30
+ { id: 'gemini-2.5-flash-lite', name: 'Gemini 2.5 Flash-Lite', provider: 'google', tags: ['fast', 'cheap'], context: 1048576, priceIn: 0.1, priceOut: 0.4 },
31
+ // ── Gemma (open weights) ──
32
+ { id: 'gemma-4-31b-it', name: 'Gemma 4 31B IT', provider: 'google', tags: ['reasoning'], context: 262144, priceIn: 0.2, priceOut: 0.8 },
33
+ { id: 'gemma-4-26b-a4b-it', name: 'Gemma 4 26B A4B IT', provider: 'google', tags: ['fast', 'reasoning'], context: 262144, priceIn: 0.2, priceOut: 0.8 },
26
34
  ],
27
35
  };
28
36
 
@@ -5,10 +5,14 @@ function getKey() {
5
5
  return getApiKey('nvidia') || process.env.NVIDIA_API_KEY || process.env.NVIDIA_NIM_API_KEY;
6
6
  }
7
7
 
8
- // Model IDs below are a curated subset of NVIDIA's live GET /v1/models registry
9
- // (fetched 2026-08-13). Stale IDs — e.g. meta/llama-3.1-405b-instruct,
10
- // deepseek-ai/deepseek-v4-flash, minimax-ai/minimax-m2.7 — return 404/410 and
11
- // were deliberately removed. Re-verify against /v1/models before adding any ID.
8
+ // Model IDs below are a curated subset of NVIDIA's live GET /v1/models
9
+ // registry (fetched + cross-checked against models.dev 2026-10-17, 81 live
10
+ // models; only chat/completions-capable IDs kept — embeddings, rerankers,
11
+ // safety/guard, document-parse, video/vision-specialist and reward models
12
+ // are excluded). Stale IDs — e.g. meta/llama-3.1-8b-instruct,
13
+ // deepseek-ai/deepseek-v4-flash-0731, z-ai/glm-5.2, openai/gpt-oss-120b —
14
+ // return 404/410 and were removed. Re-verify against /v1/models before
15
+ // adding any ID.
12
16
  const provider = {
13
17
  ...createOpenAICompatProvider({
14
18
  getKey,
@@ -17,38 +21,34 @@ const provider = {
17
21
  }),
18
22
  models: [
19
23
  // ── DeepSeek ──
20
- { id: 'deepseek-ai/deepseek-v4-flash-0731', name: 'DeepSeek V4 Flash', provider: 'nvidia', tags: ['free', 'fast'], context: 200000, priceIn: 0, priceOut: 0 },
24
+ { id: 'deepseek-ai/deepseek-v4.1-flash', name: 'DeepSeek V4.1 Flash', provider: 'nvidia', tags: ['reasoning', 'fast'], context: 1000000, priceIn: 0.14, priceOut: 0.28 },
21
25
  { id: 'deepseek-ai/deepseek-coder-6.7b-instruct', name: 'DeepSeek Coder 6.7B', provider: 'nvidia', tags: ['coding'], context: 16384, priceIn: 0.10, priceOut: 0.10 },
22
26
 
23
27
  // ── Meta Llama ──
24
- { id: 'meta/llama-3.1-8b-instruct', name: 'Llama 3.1 8B Instruct', provider: 'nvidia', tags: ['fast'], context: 128000, priceIn: 0.05, priceOut: 0.08 },
25
- { id: 'meta/llama-3.1-70b-instruct', name: 'Llama 3.1 70B Instruct', provider: 'nvidia', tags: ['general'], context: 128000, priceIn: 0.35, priceOut: 0.40 },
26
- { id: 'meta/llama-3.2-3b-instruct', name: 'Llama 3.2 3B Instruct', provider: 'nvidia', tags: ['fast'], context: 128000, priceIn: 0.02, priceOut: 0.02 },
27
28
  { id: 'meta/llama-3.2-11b-vision-instruct', name: 'Llama 3.2 11B Vision', provider: 'nvidia', tags: ['vision', 'fast'], context: 128000, priceIn: 0.055, priceOut: 0.055 },
28
29
  { id: 'meta/llama-3.2-90b-vision-instruct', name: 'Llama 3.2 90B Vision', provider: 'nvidia', tags: ['vision'], context: 128000, priceIn: 0.35, priceOut: 0.40 },
29
- { id: 'meta/llama-3.3-70b-instruct', name: 'Llama 3.3 70B Instruct', provider: 'nvidia', tags: ['general'], context: 128000, priceIn: 0.35, priceOut: 0.40 },
30
+ { id: 'meta/muse-glimmer-30b', name: 'Llama Muse Glimmer 30B', provider: 'nvidia', tags: ['creative', 'reasoning'], context: 131072, priceIn: 0.20, priceOut: 0.20 },
30
31
  { id: 'meta/codellama-70b', name: 'CodeLlama 70B', provider: 'nvidia', tags: ['coding'], context: 16384, priceIn: 0.30, priceOut: 0.30 },
31
- { id: 'meta/muse-glimmer-30b', name: 'Llama Muse Glimmer 30B', provider: 'nvidia', tags: ['creative'], context: 32768, priceIn: 0.20, priceOut: 0.20 },
32
+ { id: 'meta/llama2-70b', name: 'Llama 2 70B', provider: 'nvidia', tags: ['legacy'], context: 4096, priceIn: 0.20, priceOut: 0.20 },
32
33
 
33
34
  // ── NVIDIA Nemotron ──
34
- { id: 'nvidia/llama-3.1-nemotron-70b-instruct', name: 'Llama 3.1 Nemotron 70B', provider: 'nvidia', tags: ['general'], context: 128000, priceIn: 0.35, priceOut: 0.40 },
35
- { id: 'nvidia/llama-3.1-nemotron-ultra-253b-v1', name: 'Nemotron Ultra 253B', provider: 'nvidia', tags: ['frontier'], context: 128000, priceIn: 1.50, priceOut: 6.00 },
36
- { id: 'nvidia/llama-3.3-nemotron-super-49b-v1', name: 'Nemotron Super 49B', provider: 'nvidia', tags: ['reasoning'], context: 128000, priceIn: 0.25, priceOut: 0.90 },
37
- { id: 'nvidia/llama-3.3-nemotron-super-49b-v1.5', name: 'Nemotron Super 49B v1.5', provider: 'nvidia', tags: ['reasoning'], context: 128000, priceIn: 0.25, priceOut: 0.90 },
38
- { id: 'nvidia/nemotron-3-ultra-550b-a55b', name: 'Nemotron 3 Ultra 550B', provider: 'nvidia', tags: ['frontier'], context: 256000, priceIn: 2.00, priceOut: 8.00 },
39
- { id: 'nvidia/nemotron-3-super-120b-a12b', name: 'Nemotron 3 Super 120B', provider: 'nvidia', tags: ['frontier'], context: 256000, priceIn: 1.00, priceOut: 4.00 },
40
- { id: 'nvidia/nemotron-3-nano-30b-a3b', name: 'Nemotron 3 Nano 30B', provider: 'nvidia', tags: ['fast'], context: 128000, priceIn: 0.10, priceOut: 0.40 },
41
- { id: 'nvidia/nemotron-3-nano-omni-30b-a3b-reasoning', name: 'Nemotron 3 Nano Omni (Reasoning)', provider: 'nvidia', tags: ['reasoning', 'small'], context: 128000, priceIn: 0.10, priceOut: 0.40 },
42
- { id: 'nvidia/nemotron-4-340b-instruct', name: 'Nemotron-4 340B Instruct', provider: 'nvidia', tags: ['frontier'], context: 4096, priceIn: 1.00, priceOut: 3.00 },
43
- { id: 'nvidia/nemotron-mini-4b-instruct', name: 'Nemotron Mini 4B', provider: 'nvidia', tags: ['fast'], context: 32768, priceIn: 0.03, priceOut: 0.03 },
35
+ { id: 'nvidia/nemotron-3-ultra-550b-a55b', name: 'Nemotron 3 Ultra 550B', provider: 'nvidia', tags: ['frontier', 'reasoning'], context: 1000000, priceIn: 0.50, priceOut: 2.50 },
36
+ { id: 'nvidia/nemotron-3-super-120b-a12b', name: 'Nemotron 3 Super 120B', provider: 'nvidia', tags: ['reasoning'], context: 262144, priceIn: 0.25, priceOut: 1.00 },
37
+ { id: 'nvidia/nemotron-3-nano-omni-30b-a3b-reasoning', name: 'Nemotron 3 Nano Omni (Reasoning)', provider: 'nvidia', tags: ['reasoning', 'fast'], context: 256000, priceIn: 0.10, priceOut: 0.40 },
44
38
  { id: 'nvidia/nemotron-nano-3-30b-a3b', name: 'Nemotron Nano 3 30B', provider: 'nvidia', tags: ['fast'], context: 128000, priceIn: 0.10, priceOut: 0.40 },
45
- { id: 'nvidia/nemotron-nano-12b-v2-vl', name: 'Nemotron Nano 12B V2 VL', provider: 'nvidia', tags: ['vision'], context: 128000, priceIn: 0.10, priceOut: 0.40 },
46
- { id: 'nvidia/nvidia-nemotron-nano-9b-v2', name: 'Nemotron Nano 9B V2', provider: 'nvidia', tags: ['fast'], context: 128000, priceIn: 0.05, priceOut: 0.20 },
39
+ { id: 'nvidia/nemotron-3.5-lightning-30b-a3b', name: 'Nemotron 3.5 Lightning 30B', provider: 'nvidia', tags: ['reasoning', 'fast'], context: 262144, priceIn: 0.10, priceOut: 0.40 },
40
+ { id: 'nvidia/nemotron-3.5-super-vl-120b-a12b', name: 'Nemotron 3.5 Super VL 120B', provider: 'nvidia', tags: ['vision', 'reasoning'], context: 1000000, priceIn: 0.25, priceOut: 1.00 },
41
+ { id: 'nvidia/llama-3.1-nemotron-ultra-253b-v1', name: 'Nemotron Ultra 253B', provider: 'nvidia', tags: ['frontier', 'reasoning'], context: 128000, priceIn: 1.50, priceOut: 6.00 },
42
+ { id: 'nvidia/llama-3.1-nemotron-70b-instruct', name: 'Llama 3.1 Nemotron 70B', provider: 'nvidia', tags: ['general'], context: 128000, priceIn: 0.35, priceOut: 0.40 },
43
+ { id: 'nvidia/llama-3.1-nemotron-51b-instruct', name: 'Llama 3.1 Nemotron 51B', provider: 'nvidia', tags: ['general'], context: 128000, priceIn: 0.20, priceOut: 0.80 },
44
+ { id: 'nvidia/llama3-chatqa-1.5-70b', name: 'Llama3 ChatQA 1.5 70B', provider: 'nvidia', tags: ['general'], context: 8192, priceIn: 0.30, priceOut: 0.30 },
45
+ { id: 'nvidia/mistral-nemo-minitron-8b-8k-instruct', name: 'Mistral NeMo Minitron 8B', provider: 'nvidia', tags: ['fast'], context: 8192, priceIn: 0.05, priceOut: 0.05 },
46
+ { id: 'nvidia/nemotron-4-340b-instruct', name: 'Nemotron-4 340B Instruct', provider: 'nvidia', tags: ['frontier', 'legacy'], context: 4096, priceIn: 1.00, priceOut: 3.00 },
47
47
 
48
48
  // ── Mistral ──
49
- { id: 'mistralai/mistral-7b-instruct-v0.3', name: 'Mistral 7B Instruct', provider: 'nvidia', tags: ['fast'], context: 32768, priceIn: 0.05, priceOut: 0.05 },
50
49
  { id: 'mistralai/mistral-large', name: 'Mistral Large', provider: 'nvidia', tags: ['frontier'], context: 128000, priceIn: 2.00, priceOut: 6.00 },
51
50
  { id: 'mistralai/mistral-large-2-instruct', name: 'Mistral Large 2 Instruct', provider: 'nvidia', tags: ['frontier'], context: 128000, priceIn: 2.00, priceOut: 6.00 },
51
+ { id: 'mistralai/mistral-7b-instruct-v0.3', name: 'Mistral 7B Instruct', provider: 'nvidia', tags: ['fast'], context: 65536, priceIn: 0.05, priceOut: 0.05 },
52
52
  { id: 'mistralai/codestral-22b-instruct-v0.1', name: 'Codestral 22B', provider: 'nvidia', tags: ['coding'], context: 32768, priceIn: 0.30, priceOut: 0.60 },
53
53
  { id: 'mistralai/mixtral-8x22b-v0.1', name: 'Mixtral 8x22B', provider: 'nvidia', tags: [], context: 65536, priceIn: 0.60, priceOut: 0.60 },
54
54
  { id: 'nv-mistralai/mistral-nemo-12b-instruct', name: 'Mistral NeMo 12B', provider: 'nvidia', tags: ['fast'], context: 128000, priceIn: 0.05, priceOut: 0.05 },
@@ -58,26 +58,38 @@ const provider = {
58
58
  { id: 'microsoft/phi-3-vision-128k-instruct', name: 'Phi 3 Vision 128K', provider: 'nvidia', tags: ['vision'], context: 128000, priceIn: 0.05, priceOut: 0.05 },
59
59
 
60
60
  // ── Google ──
61
- { id: 'google/gemma-3-4b-it', name: 'Gemma 3 4B IT', provider: 'nvidia', tags: ['fast'], context: 32768, priceIn: 0.03, priceOut: 0.03 },
62
- { id: 'google/gemma-3-12b-it', name: 'Gemma 3 12B IT', provider: 'nvidia', tags: ['general'], context: 128000, priceIn: 0.10, priceOut: 0.10 },
63
- { id: 'google/gemma-4-31b-it', name: 'Gemma 4 31B IT', provider: 'nvidia', tags: ['general'], context: 32768, priceIn: 0.20, priceOut: 0.80 },
61
+ { id: 'google/gemma-4-31b-it', name: 'Gemma 4 31B IT', provider: 'nvidia', tags: ['general', 'reasoning'], context: 256000, priceIn: 0.20, priceOut: 0.80 },
62
+ { id: 'google/gemma-3-12b-it', name: 'Gemma 3 12B IT', provider: 'nvidia', tags: ['general'], context: 131072, priceIn: 0.10, priceOut: 0.10 },
63
+ { id: 'google/gemma-3-4b-it', name: 'Gemma 3 4B IT', provider: 'nvidia', tags: ['fast'], context: 131072, priceIn: 0.03, priceOut: 0.03 },
64
+ { id: 'google/diffusiongemma-26b-a4b-it', name: 'DiffusionGemma 26B', provider: 'nvidia', tags: ['reasoning'], context: 250000, priceIn: 0.20, priceOut: 0.80 },
64
65
  { id: 'google/codegemma-1.1-7b', name: 'CodeGemma 1.1 7B', provider: 'nvidia', tags: ['coding', 'fast'], context: 16384, priceIn: 0.05, priceOut: 0.05 },
66
+ { id: 'google/codegemma-7b', name: 'CodeGemma 7B', provider: 'nvidia', tags: ['coding'], context: 16384, priceIn: 0.05, priceOut: 0.05 },
65
67
 
66
68
  // ── OpenAI ──
67
- { id: 'openai/gpt-oss-120b', name: 'GPT-OSS 120B', provider: 'nvidia', tags: ['openai', 'general'], context: 131072, priceIn: 0.25, priceOut: 1.00 },
68
- { id: 'openai/gpt-oss-20b', name: 'GPT-OSS 20B', provider: 'nvidia', tags: ['openai', 'fast'], context: 131072, priceIn: 0.10, priceOut: 0.30 },
69
+ { id: 'openai/gpt-oss-20b', name: 'GPT-OSS 20B', provider: 'nvidia', tags: ['openai', 'reasoning', 'fast'], context: 131072, priceIn: 0.10, priceOut: 0.30 },
70
+
71
+ // ── Z.ai / Moonshot ──
72
+ { id: 'z-ai/glm-5.3', name: 'GLM-5.3', provider: 'nvidia', tags: ['reasoning'], context: 1000000, priceIn: 0.30, priceOut: 1.20 },
73
+ { id: 'z-ai/glm-5.3-flash', name: 'GLM-5.3 Flash', provider: 'nvidia', tags: ['fast', 'reasoning'], context: 1000000, priceIn: 0.05, priceOut: 0.20 },
74
+ { id: 'moonshotai/kimi-k2.6', name: 'Kimi K2.6', provider: 'nvidia', tags: ['reasoning'], context: 262144, priceIn: 0.20, priceOut: 0.80 },
69
75
 
70
76
  // ── Other ──
71
- { id: 'z-ai/glm-5.2', name: 'GLM-5.2', provider: 'nvidia', tags: [], context: 131072, priceIn: 0.30, priceOut: 1.20 },
72
- { id: 'moonshotai/kimi-k2.6', name: 'Kimi K2.6', provider: 'nvidia', tags: [], context: 128000, priceIn: 0.20, priceOut: 0.80 },
73
- { id: 'minimaxai/minimax-m3', name: 'MiniMax M3', provider: 'nvidia', tags: [], context: 200000, priceIn: 0.40, priceOut: 1.60 },
74
- { id: 'poolside/laguna-xs-2.1', name: 'Laguna XS 2.1', provider: 'nvidia', tags: ['free', 'coding'], context: 131072, priceIn: 0, priceOut: 0 },
75
- { id: 'stepfun-ai/step-3.7-flash', name: 'Step 3.7 Flash', provider: 'nvidia', tags: ['fast'], context: 128000, priceIn: 0.15, priceOut: 0.40 },
77
+ { id: 'poolside/laguna-xs-2.1', name: 'Laguna XS 2.1', provider: 'nvidia', tags: ['free', 'coding', 'reasoning'], context: 262144, priceIn: 0, priceOut: 0 },
78
+ { id: 'stepfun-ai/step-5-preview', name: 'Step 5 Preview', provider: 'nvidia', tags: ['reasoning'], context: 256000, priceIn: 0.30, priceOut: 1.20 },
79
+ { id: 'writer/palmyra-creative-122b', name: 'Palmyra Creative 122B', provider: 'nvidia', tags: ['creative'], context: 128000, priceIn: 0.90, priceOut: 0.90 },
80
+ { id: 'writer/palmyra-fin-70b-32k', name: 'Palmyra Fin 70B', provider: 'nvidia', tags: ['finance'], context: 32768, priceIn: 0.90, priceOut: 0.90 },
81
+ { id: 'writer/palmyra-med-70b', name: 'Palmyra Med 70B', provider: 'nvidia', tags: ['medical'], context: 128000, priceIn: 0.90, priceOut: 0.90 },
82
+ { id: 'writer/palmyra-med-70b-32k', name: 'Palmyra Med 70B 32K', provider: 'nvidia', tags: ['medical'], context: 32768, priceIn: 0.90, priceOut: 0.90 },
76
83
  { id: '01-ai/yi-large', name: 'Yi Large', provider: 'nvidia', tags: [], context: 32768, priceIn: 0.30, priceOut: 0.90 },
77
84
  { id: 'ai21labs/jamba-1.5-large-instruct', name: 'Jamba 1.5 Large', provider: 'nvidia', tags: [], context: 256000, priceIn: 0.20, priceOut: 0.80 },
85
+ { id: 'aisingapore/sea-lion-7b-instruct', name: 'Sea-Lion 7B Instruct', provider: 'nvidia', tags: ['fast'], context: 8192, priceIn: 0.05, priceOut: 0.05 },
78
86
  { id: 'databricks/dbrx-instruct', name: 'DBRX Instruct', provider: 'nvidia', tags: [], context: 32768, priceIn: 0.60, priceOut: 0.60 },
79
87
  { id: 'ibm/granite-3.0-8b-instruct', name: 'Granite 3.0 8B', provider: 'nvidia', tags: ['fast'], context: 32768, priceIn: 0.05, priceOut: 0.05 },
88
+ { id: 'ibm/granite-3.0-3b-a800m-instruct', name: 'Granite 3.0 3B MoE', provider: 'nvidia', tags: ['fast'], context: 32768, priceIn: 0.03, priceOut: 0.03 },
80
89
  { id: 'ibm/granite-34b-code-instruct', name: 'Granite 34B Code', provider: 'nvidia', tags: ['coding'], context: 16384, priceIn: 0.20, priceOut: 0.20 },
90
+ { id: 'ibm/granite-8b-code-instruct', name: 'Granite 8B Code', provider: 'nvidia', tags: ['coding', 'fast'], context: 16384, priceIn: 0.10, priceOut: 0.10 },
91
+ { id: 'nvidia/riva-translate-4b-instruct', name: 'Riva Translate 4B', provider: 'nvidia', tags: ['fast', 'translation'], context: 32768, priceIn: 0.05, priceOut: 0.05 },
92
+ { id: 'nvidia/riva-translate-4b-instruct-v2', name: 'Riva Translate 4B v2', provider: 'nvidia', tags: ['fast', 'translation'], context: 32768, priceIn: 0.05, priceOut: 0.05 },
81
93
  { id: 'zyphra/zamba2-7b-instruct', name: 'Zamba2 7B', provider: 'nvidia', tags: ['fast'], context: 32768, priceIn: 0.05, priceOut: 0.05 },
82
94
  ],
83
95
  };