openzoo 0.50.87 → 0.50.89
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 +15 -2
- package/lib/models.js +30 -0
- package/lib/proxy.js +19 -1
- package/lib/websearch.js +31 -0
- package/package.json +1 -1
package/bin/openzoo.js
CHANGED
|
@@ -330,7 +330,7 @@ async function main() {
|
|
|
330
330
|
}
|
|
331
331
|
case 'ask': {
|
|
332
332
|
const question = process.argv[3];
|
|
333
|
-
if (!question) throw new Error('usage: openzoo ask "<question>" [--context <id>] [--model <id>] [--system <text>]');
|
|
333
|
+
if (!question) throw new Error('usage: openzoo ask "<question>" [--context <id>] [--model <id>] [--system <text>] [--web [--web-results N]]');
|
|
334
334
|
const ci = process.argv.indexOf('--context');
|
|
335
335
|
const mi = process.argv.indexOf('--model');
|
|
336
336
|
// A BARE QUESTION IS A DIFFERENT PRODUCT FROM A BRIEFED ONE.
|
|
@@ -344,7 +344,20 @@ async function main() {
|
|
|
344
344
|
// DHH, Hyprland and theming. Same gateway, same product, one had context.
|
|
345
345
|
// A caller that knows where it is running can now say so.
|
|
346
346
|
const si = process.argv.indexOf('--system');
|
|
347
|
-
|
|
347
|
+
let system = si !== -1 ? process.argv[si + 1] : '';
|
|
348
|
+
// --web: a keyless DuckDuckGo search, top results injected into THIS
|
|
349
|
+
// call's system prompt. The x402 rail strips OpenRouter's `plugins`
|
|
350
|
+
// field, so search-then-inject has to happen here, on the caller's
|
|
351
|
+
// side. EGRESS: the question text goes to duckduckgo.com. Also on with
|
|
352
|
+
// OPENZOO_ASK_WEB=1; --web-results N caps the count (default 5).
|
|
353
|
+
const wantWeb = process.argv.includes('--web') || process.env.OPENZOO_ASK_WEB === '1';
|
|
354
|
+
if (wantWeb) {
|
|
355
|
+
const wi = process.argv.indexOf('--web-results');
|
|
356
|
+
const n = wi !== -1 ? Number(process.argv[wi + 1]) || 5 : 5;
|
|
357
|
+
const { webSearch, formatWebResults } = await import('../lib/websearch.js');
|
|
358
|
+
const hits = await webSearch(question, n).catch((e) => { console.error(`web search failed: ${e.message}`); return []; });
|
|
359
|
+
if (hits.length) system = (system ? system + '\n\n' : '') + formatWebResults(question, hits);
|
|
360
|
+
}
|
|
348
361
|
const { PayClient } = await import('../lib/pay.js');
|
|
349
362
|
const { config } = await import('../lib/config.js');
|
|
350
363
|
const client = new PayClient();
|
package/lib/models.js
CHANGED
|
@@ -168,9 +168,39 @@ const tokensOf = (id) => id.toLowerCase().split(/[^a-z0-9.]+/).filter((t) => t &
|
|
|
168
168
|
* the id is already servable (no rewrite), otherwise the closest zoo id.
|
|
169
169
|
* OPENZOO_DEFAULT_MODEL is an explicit user override, not a fallback tier.
|
|
170
170
|
*/
|
|
171
|
+
/**
|
|
172
|
+
* UNOPENROUTER. OpenRouter is not an upstream any more (gateway, 2026-09-02):
|
|
173
|
+
* every completion is bought from an x402 door, and doors publish BARE ids
|
|
174
|
+
* (`grok-4.3`, `claude-sonnet-5`, `gemini-2.5-flash`). A harness that still
|
|
175
|
+
* sends the OpenRouter spelling (`x-ai/grok-4.3`, `anthropic/claude-sonnet-5`)
|
|
176
|
+
* is rewritten to the bare id whenever the live catalog serves that bare id.
|
|
177
|
+
*
|
|
178
|
+
* OPENZOO_UNOPENROUTER=1 strip the vendor prefix ALWAYS, catalog or not
|
|
179
|
+
* OPENZOO_UNOPENROUTER=0 never strip
|
|
180
|
+
* unset strip when the bare id is in the catalog
|
|
181
|
+
*
|
|
182
|
+
* Router aliases and `openzoo-` twins are never touched: their slash is not a
|
|
183
|
+
* vendor.
|
|
184
|
+
*/
|
|
185
|
+
export function unopenrouter(requested, ids) {
|
|
186
|
+
const mode = process.env.OPENZOO_UNOPENROUTER;
|
|
187
|
+
if (mode === '0') return null;
|
|
188
|
+
const s = String(requested || '');
|
|
189
|
+
if (!s || isAutoModel(s) || /^openzoo[-/]/i.test(s)) return null;
|
|
190
|
+
const slash = s.indexOf('/');
|
|
191
|
+
if (slash <= 0) return null;
|
|
192
|
+
const bare = s.slice(slash + 1);
|
|
193
|
+
if (!bare || bare.includes('/')) return null;
|
|
194
|
+
if (mode === '1') return bare;
|
|
195
|
+
return Array.isArray(ids) && ids.includes(bare) ? bare : null;
|
|
196
|
+
}
|
|
197
|
+
|
|
171
198
|
export function resolveModel(requested, ids) {
|
|
172
199
|
// Virtual router id — never family-match, never steal via OPENZOO_DEFAULT_MODEL.
|
|
173
200
|
if (isAutoModel(requested)) return null;
|
|
201
|
+
// Vendor-prefixed OpenRouter spelling → the bare id the doors serve.
|
|
202
|
+
const bareVendor = unopenrouter(requested, ids);
|
|
203
|
+
if (bareVendor) return bareVendor;
|
|
174
204
|
// Bare Anthropic / Claude Code ids are never live on Fly/OpenRouter
|
|
175
205
|
// (`claude-opus-5` → 500 unknown model). Rewrite even on a catalog miss
|
|
176
206
|
// or if a gateway row lists the bare name — the request must not leave
|
package/lib/proxy.js
CHANGED
|
@@ -13,7 +13,7 @@ import { withOnrampLink } from './stripeOnramp.js';
|
|
|
13
13
|
import { tokenBalance } from './x402.js';
|
|
14
14
|
import { evmTokenBalance } from './evm.js';
|
|
15
15
|
import { autoContext } from './autobind.js';
|
|
16
|
-
import { modelsListForRequest, isHarnessAliasId, resolveModel, quoteableRows } from './models.js';
|
|
16
|
+
import { modelsListForRequest, isHarnessAliasId, resolveModel, quoteableRows, unopenrouter } from './models.js';
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* Quoteable catalog ids, cached 5 minutes, for the fuzzy /v1/models/<id> probe.
|
|
@@ -622,6 +622,24 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
622
622
|
servedRequests += 1;
|
|
623
623
|
say(`\n<- request #${servedRequests} from ${(req.headers['user-agent'] || 'unknown').slice(0, 40)}`);
|
|
624
624
|
try { wantsStream = JSON.parse(bodyBuf.toString('utf8'))?.stream === true; } catch { /* not JSON */ }
|
|
625
|
+
// UNOPENROUTER THE MODEL ID before anything downstream sees the body —
|
|
626
|
+
// the replay key, the outage gate, the wire. A vendor-prefixed
|
|
627
|
+
// OpenRouter spelling becomes the bare id the doors serve when the
|
|
628
|
+
// catalog lists it (OPENZOO_UNOPENROUTER=1 forces it, =0 disables).
|
|
629
|
+
// This is the one place the body is rewritten; see models.js.
|
|
630
|
+
try {
|
|
631
|
+
const parsed = JSON.parse(bodyBuf.toString('utf8'));
|
|
632
|
+
if (parsed && typeof parsed.model === 'string') {
|
|
633
|
+
let ids = [];
|
|
634
|
+
try { ids = await catalogIdsCached(`${config.apiBase}/v1/models`, upstreamHeaders(req)); } catch { /* catalog unreachable: only the forced mode rewrites */ }
|
|
635
|
+
const bare = unopenrouter(parsed.model, ids);
|
|
636
|
+
if (bare) {
|
|
637
|
+
say(` model ${parsed.model} -> ${bare} (bare id: doors, not OpenRouter)`);
|
|
638
|
+
parsed.model = bare;
|
|
639
|
+
bodyBuf = Buffer.from(JSON.stringify(parsed));
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
} catch { /* not JSON */ }
|
|
625
643
|
}
|
|
626
644
|
|
|
627
645
|
// Retry of a body we answered seconds ago? Serve the cached completion —
|
package/lib/websearch.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Keyless web search for `openzoo ask --web`: DuckDuckGo's HTML endpoint,
|
|
2
|
+
// scraped for title / url / snippet. No API key, no account, one GET. The
|
|
3
|
+
// only thing that leaves is the question text, to duckduckgo.com.
|
|
4
|
+
const strip = (s) => String(s || '')
|
|
5
|
+
.replace(/<[^>]+>/g, '')
|
|
6
|
+
.replace(/&/g, '&').replace(/"/g, '"').replace(/'/g, "'").replace(/</g, '<').replace(/>/g, '>')
|
|
7
|
+
.replace(/\s+/g, ' ').trim();
|
|
8
|
+
|
|
9
|
+
export async function webSearch(query, max = 5) {
|
|
10
|
+
const res = await fetch('https://html.duckduckgo.com/html/?q=' + encodeURIComponent(query), {
|
|
11
|
+
headers: { 'user-agent': 'Mozilla/5.0 openzoo-ask/1.0' },
|
|
12
|
+
signal: AbortSignal.timeout(12_000),
|
|
13
|
+
});
|
|
14
|
+
if (!res.ok) throw new Error(`duckduckgo HTTP ${res.status}`);
|
|
15
|
+
const html = await res.text();
|
|
16
|
+
const out = [];
|
|
17
|
+
const re = /<a[^>]*class="result__a"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>[\s\S]*?class="result__snippet"[^>]*>([\s\S]*?)<\/a>/g;
|
|
18
|
+
let m;
|
|
19
|
+
while ((m = re.exec(html)) && out.length < Math.max(1, Math.min(10, max))) {
|
|
20
|
+
let url = m[1];
|
|
21
|
+
const redirected = url.match(/uddg=([^&]+)/);
|
|
22
|
+
if (redirected) url = decodeURIComponent(redirected[1]);
|
|
23
|
+
out.push({ title: strip(m[2]), url, snippet: strip(m[3]).slice(0, 400) });
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function formatWebResults(query, hits) {
|
|
29
|
+
const lines = hits.map((h, i) => `${i + 1}. ${h.title} — ${h.url}\n ${h.snippet}`);
|
|
30
|
+
return `Web search results for "${query}" (DuckDuckGo, fetched just now; cite the url when you rely on one):\n${lines.join('\n')}`;
|
|
31
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.50.
|
|
3
|
+
"version": "0.50.89",
|
|
4
4
|
"description": "Local x402-paying proxy + MCP server for openzoo.fun — point any OpenAI-compatible harness (Cursor, Claude Code, aider, SDKs) at localhost and it pays per call from a local burner wallet. Solana and Base rails live; Robinhood experimental.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|