openzoo 0.49.18 → 0.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/openzoo.js CHANGED
@@ -106,10 +106,6 @@ usage:
106
106
  by default, --terminal for the Claude Code CLI
107
107
  npx openzoo launch <cmd> [args] launch a TERMINAL Messages API client
108
108
  (claude, aider...) already pointed at the zoo
109
- npx openzoo grokbot KEEP Grok Bot's UI, serve YOUR RunPod box under it:
110
- spawns a CPU box, MITMs api2.cursor.sh, and answers
111
- EnsureSandBox with your box instead of a cursorvm pod.
112
- Inference x402-paid; needs RUNPOD_API_KEY + sudo.
113
109
  npx openzoo grok-cli point the grok CLI at the zoo — GROK MODELS ONLY,
114
110
  paid per call by x402 instead of xAI first-party billing,
115
111
  then TAKE OVER the app's backend: pins api2.cursor.sh in
@@ -148,8 +144,6 @@ env:
148
144
  OPENZOO_RAIL (unset — force a rail: solana | base | robinhood)
149
145
  OPENZOO_BASE_RPC (https://mainnet.base.org) OPENZOO_RH_RPC (rpc.mainnet.chain.robinhood.com)
150
146
  OPENZOO_MAX_USD_PER_CALL (unset — NO per-call ceiling; set to add one) OPENZOO_DEMO_MAX_USD (0.01)
151
- OPENZOO_CONTEXT_MIN_CHARS (16384 — bodies bigger than this bind once + reuse)
152
- OPENZOO_NO_CONTEXT_CACHE (0 — set 1 to always ship the full body)
153
147
  OPENZOO_ENABLE_RH (0 — let DEFAULT selection fall through to the Robinhood rail;
154
148
  OPENZOO_RAIL=robinhood forces it without this)
155
149
  OPENZOO_TUNNEL_MAX_USD (unset — NO public-url session ceiling; set to add one) OPENZOO_TUNNEL_TOKEN (pin the api key)
@@ -167,9 +161,6 @@ async function main() {
167
161
  // GUI editors read config files, not env vars — see lib/setup.js.
168
162
  await (await import('../lib/setup.js')).setupEditor(cmd === 'editor' ? undefined : cmd, process.argv[3]);
169
163
  break;
170
- case 'grokbot':
171
- await (await import('../lib/grokbot.js')).runGrokBot(process.argv.slice(3));
172
- break;
173
164
  case 'grok-cli':
174
165
  case 'grok':
175
166
  // Grok Bot (com.anysphere.sand) fronts the `grok` CLI, and the CLI reads
package/lib/models.js CHANGED
@@ -1,11 +1,30 @@
1
1
  import { config } from './config.js';
2
2
  import { fetchHeaders } from './fetch.js';
3
- import {
4
- AUTO_MODEL_ID, autoHasPricedModels, autoModelListEntry, isAutoModel,
5
- isPricedTokenPair, isUnservableRouteId,
6
- } from './modelroute.js';
3
+ // AUTO routing lives on the backend now; these helpers survive only so the
4
+ // published catalog can EXCLUDE ids the gateway cannot serve (auto aliases,
5
+ // :batch twins, $0 rows). The shim never routes.
6
+ export const AUTO_MODEL_ID = 'openzoo/auto';
7
+ export const AUTO_MODEL_ALIASES = new Set(['openzoo/auto', 'openzoo-auto', 'auto']);
7
8
 
8
- export { AUTO_MODEL_ID, isAutoModel, isPricedTokenPair, isUnservableRouteId };
9
+ export function isAutoModel(id) {
10
+ return AUTO_MODEL_ALIASES.has(String(id || '').trim().toLowerCase());
11
+ }
12
+
13
+ /** Ids the gateway cannot quote for a chat turn (:batch twins, ~latest
14
+ * pointers, openzoo-* aliases). The published catalog must never list them. */
15
+ export function isUnservableRouteId(id) {
16
+ const s = String(id || '').trim();
17
+ if (!s) return true;
18
+ if (isAutoModel(s)) return false;
19
+ return s.includes(':batch') || s.startsWith('~') || s.startsWith('openzoo-');
20
+ }
21
+
22
+ /** Both sides must be a finite positive token price. $0 / NaN / missing 500. */
23
+ export function isPricedTokenPair(priceIn, priceOut) {
24
+ const pin = Number(priceIn);
25
+ const pout = Number(priceOut);
26
+ return Number.isFinite(pin) && Number.isFinite(pout) && pin > 0 && pout > 0;
27
+ }
9
28
 
10
29
  /** Same threshold as BIND_MIN_CHARS in hrr.js — kept local so this
11
30
  * module stays importable without the wallet/rpc stack. */
@@ -313,14 +332,9 @@ export function augmentModelList(payload, { aliases: withAliases = true } = {})
313
332
  };
314
333
  })
315
334
  : [];
316
- const virtual = [];
317
- // Auto is listed only when its own shortlist is priced models — never an
318
- // unquoted OpenRouter id that 500s `bad openrouter price`.
319
- if (!have.has(AUTO_MODEL_ID) && autoHasPricedModels(undefined, ids.length ? ids : null)) {
320
- virtual.push(autoModelListEntry());
321
- have.add(AUTO_MODEL_ID);
322
- }
323
- return { ...payload, object: payload?.object || 'list', data: [...virtual, ...data, ...aliases] };
335
+ // Auto is no longer published: routing lives on the backend, and a shim
336
+ // that advertises openzoo/auto would have to route it.
337
+ return { ...payload, object: payload?.object || 'list', data: [...data, ...aliases] };
324
338
  }
325
339
 
326
340
  /**
@@ -415,7 +429,6 @@ export function pickClaudePickerRows(rows) {
415
429
  for (const { prefer, re } of CLAUDE_PICKER_GATEWAY) {
416
430
  add(ids.includes(prefer) ? prefer : newestMatching(ids, (id) => re.test(id)));
417
431
  }
418
- add(AUTO_MODEL_ID);
419
432
  return picked.map((id) => byId.get(id)).filter(Boolean);
420
433
  }
421
434
 
@@ -462,200 +475,3 @@ export function wantsAnthropicModelList(headers = {}) {
462
475
  export function modelsListForRequest(payload, headers) {
463
476
  return wantsAnthropicModelList(headers) ? anthropicModelList(payload) : publishModelList(payload);
464
477
  }
465
-
466
- /**
467
- * Which request paths carry a rewritable model field. POST-only; embeddings /
468
- * audio / image / moderation models are DIFFERENT model families — rewriting
469
- * a chat model into those would corrupt the call, so they pass untouched.
470
- */
471
- export function rewritablePath(method, url) {
472
- if (method !== 'POST') return false;
473
- const p = (url || '').split('?')[0];
474
- return !/embed|audio|image|moderation/.test(p);
475
- }
476
-
477
- /**
478
- * Families that spend max_tokens on hidden thinking first. A 16/40/160
479
- * budget on these returns an empty visible completion — measured on Grok
480
- * and DeepSeek. The raise in raiseReasoningMaxTokens exists for that.
481
- * It must NEVER fire on Claude Code's 16-token auto-mode classifier.
482
- */
483
- export const REASONING_MODEL_RE = /(deepseek|grok|o[134](-|$)|reasoner|thinking|-pro\b|sol-pro|qwq)/i;
484
-
485
- /** Claude Code auto-mode classify is max_tokens=16. 0.48.75 used 64 and
486
- * missed grok nubs at 128 / 2000 on a 1–2 message body (dead-steady ~3¢
487
- * from the reasoning floor). Anything in (0, 256] on a short transcript
488
- * is a nub; a fat grok chat still uses the 4000 floor. */
489
- export const CLASSIFY_MAX_TOKENS = 256;
490
- export const CLASSIFY_MAX_MSGS = 3;
491
- export const CLASSIFY_MAX_BODY = 65_536;
492
-
493
- const CLASSIFIER_PREFS = ['google/gemini-3.7-flash', 'anthropic/claude-haiku-4.5'];
494
-
495
- function messageHasToolCalls(m) {
496
- return Boolean(
497
- (Array.isArray(m?.tool_calls) && m.tool_calls.length)
498
- || m?.function_call
499
- || m?.role === 'tool',
500
- );
501
- }
502
-
503
- /**
504
- * Tiny classify / grok nub: pin to flash, never apply the reasoning floor.
505
- *
506
- * 0.48.75 required max_tokens ≤ 64 AND body < BIND_MIN (16k). Live 3¢
507
- * asks missed that (max_tokens 128 or 2000, and/or body ≥ 16k from a
508
- * tools schema) and then ate the 4000 grok floor. Widen:
509
- * - max_tokens ≤ 256 on a short transcript (≤ 6 msgs, no tool_calls)
510
- * - OR few messages, no tool_calls, body under a few tens of KB
511
- * (even when the caller asked for 2000 tokens)
512
- * A real grok chat (max_tokens 2000+ AND a long / tool-using transcript)
513
- * still returns false so the floor can fire.
514
- *
515
- * `body` may be the raw Buffer/string or a parsed object. An optional
516
- * `bodyLen` overrides stringify length when the caller still has the wire
517
- * bytes (the proxy does).
518
- */
519
- export function isTinyClassify(body, bodyLen) {
520
- let parsed = body;
521
- let len = bodyLen;
522
- if (body == null) return false;
523
- if (typeof body === 'string' || Buffer.isBuffer(body)) {
524
- const buf = Buffer.isBuffer(body) ? body : Buffer.from(body);
525
- len = buf.length;
526
- try { parsed = JSON.parse(buf.toString('utf8')); } catch { return false; }
527
- } else if (len == null) {
528
- try { len = Buffer.byteLength(JSON.stringify(body)); } catch { return false; }
529
- }
530
- const mt = Number(parsed?.max_tokens);
531
- if (!Number.isFinite(mt) || mt <= 0) return false;
532
- const messages = Array.isArray(parsed?.messages) ? parsed.messages : [];
533
- if (messages.some(messageHasToolCalls)) return false;
534
- if (mt <= CLASSIFY_MAX_TOKENS && messages.length <= 6) return true;
535
- return messages.length <= CLASSIFY_MAX_MSGS && len < CLASSIFY_MAX_BODY;
536
- }
537
-
538
- /**
539
- * Ids that must never serve Claude Code / grokui AUTO's tiny yes/no classify.
540
- * REASONING_MODEL_RE is the thinking floor; HEAVY_RE is the flagship set
541
- * (opus/pro/max/…). opus-5 is openzoo's default session model and does NOT
542
- * match the reasoning regex, so a catalog that lists opus before flash — or
543
- * lists only opus + grok — used to pick opus as "first non-reasoner". That
544
- * classify is a 402 handshake on a big model; AUTO's timeout then hard-blocks
545
- * Bash instead of prompting.
546
- */
547
- function isSlowClassifier(id) {
548
- const s = String(id || '');
549
- return REASONING_MODEL_RE.test(s) || HEAVY_RE.test(s);
550
- }
551
-
552
- /**
553
- * Fast non-reasoning id that is actually on the zoo. Prefer an explicit
554
- * OPENZOO_CLASSIFIER_MODEL, then flash, then haiku, then the first catalog
555
- * id that is neither a reasoner nor a heavy/flagship (opus/pro/max/…).
556
- */
557
- export function pickClassifierModel(ids, preferred = process.env.OPENZOO_CLASSIFIER_MODEL) {
558
- if (!Array.isArray(ids) || !ids.length) return null;
559
- if (preferred && ids.includes(preferred)) return preferred;
560
- for (const id of CLASSIFIER_PREFS) {
561
- if (ids.includes(id)) return id;
562
- }
563
- return ids.find((id) => !isSlowClassifier(id)) || null;
564
- }
565
-
566
- /**
567
- * Raise max_tokens for a reasoning model. Returns { parsed, raised, from, to }.
568
- * Does not itself decide whether a request is a classify — callers skip this
569
- * when isTinyClassify is true. Real Grok/DeepSeek chats still need the floor:
570
- * 4× a caller's 40 is 160, and those still come back blank.
571
- */
572
- export function raiseReasoningMaxTokens(parsed, env = process.env) {
573
- const mult = Number(env.OPENZOO_REASONING_MAX_TOKENS_X || 4);
574
- const cap = Number(env.OPENZOO_REASONING_MAX_TOKENS_CAP || 32000);
575
- const floor = Number(env.OPENZOO_REASONING_MIN_TOKENS || 4000);
576
- const mdl = String(parsed?.model || '');
577
- const mt = Number(parsed?.max_tokens);
578
- if (mult > 1 && REASONING_MODEL_RE.test(mdl) && Number.isFinite(mt) && mt > 0 && mt < cap) {
579
- const raised = Math.min(cap, Math.max(floor, Math.round(mt * mult)));
580
- if (raised > mt) {
581
- return { parsed: { ...parsed, max_tokens: raised }, raised: true, from: mt, to: raised };
582
- }
583
- }
584
- return { parsed, raised: false, from: mt, to: mt };
585
- }
586
-
587
- /**
588
- * Model + max_tokens policy for one chat body.
589
- *
590
- * Tiny classify: pin to a fast non-reasoning catalog id, leave max_tokens
591
- * alone, ignore OPENZOO_DEFAULT_MODEL. Never fall back to `from` when that
592
- * id is a reasoner or a heavy/flagship (the zoo default is opus-5). A
593
- * catalog miss or an opus-only list used to keep the classify on opus-5
594
- * and AUTO hard-blocked Bash. Everything else: resolveModel (which honours
595
- * the default) then the reasoning floor.
596
- */
597
- export function rewriteChatModel(parsed, ids, { bodyLen } = {}) {
598
- const from = parsed?.model;
599
- const len = bodyLen ?? (parsed == null ? 0 : Buffer.byteLength(JSON.stringify(parsed)));
600
- if (isAutoModel(from) && !isTinyClassify(parsed, len)) {
601
- return { parsed, tiny: false, auto: true, from, to: AUTO_MODEL_ID, raised: false };
602
- }
603
- if (isTinyClassify(parsed, len)) {
604
- const picked = pickClassifierModel(ids);
605
- // pickClassifierModel returns null on an empty catalog or a zoo that
606
- // only lists reasoners/heavies. `(picked) || from` left those on
607
- // anthropic/claude-opus-5 (openzoo's default). Pin to flash instead —
608
- // never ship a classify body AUTO would time out and hard-block on.
609
- const to = picked || (typeof from === 'string' && !isSlowClassifier(from) ? from : CLASSIFIER_PREFS[0]);
610
- return {
611
- parsed: (to && to !== from) ? { ...parsed, model: to } : parsed,
612
- tiny: true,
613
- from,
614
- to,
615
- raised: false,
616
- };
617
- }
618
- if (typeof from !== 'string') {
619
- return { parsed, tiny: false, from, to: from, raised: false };
620
- }
621
- const resolved = resolveModel(from, ids) || anthropicNativeAlias(from);
622
- const next = resolved ? { ...parsed, model: resolved } : parsed;
623
- const bump = raiseReasoningMaxTokens(next);
624
- return {
625
- parsed: bump.parsed,
626
- tiny: false,
627
- from,
628
- to: next.model,
629
- raised: bump.raised,
630
- raisedFrom: bump.from,
631
- raisedTo: bump.to,
632
- };
633
- }
634
-
635
- /**
636
- * Rewrite the model field of any request body that has one.
637
- * Returns null (send as-is) or { body, from, to, tiny?, raised? }. Any
638
- * failure — bad JSON, unreachable catalog — returns null: this layer must
639
- * never break a call that would have worked without it.
640
- *
641
- * Tiny classify is pinned here too, so OPENZOO_DEFAULT_MODEL cannot capture
642
- * a 16-token yes/no even if a caller only goes through this helper.
643
- */
644
- export async function maybeRewriteModel(bodyBuf) {
645
- let body;
646
- try { body = JSON.parse(bodyBuf.toString('utf8')); } catch { return null; }
647
- if (typeof body?.model !== 'string') return null;
648
- let ids;
649
- try { ids = await zooModelIds(); } catch { return null; }
650
- const policy = rewriteChatModel(body, ids, { bodyLen: bodyBuf.length });
651
- if (!policy.tiny && !policy.raised && policy.to === body.model) return null;
652
- return {
653
- body: Buffer.from(JSON.stringify(policy.parsed)),
654
- from: body.model,
655
- to: policy.parsed.model,
656
- tiny: policy.tiny,
657
- raised: policy.raised,
658
- raisedFrom: policy.raisedFrom,
659
- raisedTo: policy.raisedTo,
660
- };
661
- }