openzoo 0.48.52 → 0.48.54

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/lib/brief.js CHANGED
@@ -19,24 +19,22 @@
19
19
  * not an API) because nothing in the conversation named the real one.
20
20
  */
21
21
  export const briefFor = (selfUrl) => [
22
- 'You are connected through an openzoo proxy (openzoo.fun), not a stock OpenAI endpoint. What that changes:',
22
+ 'Environment notes for this endpoint (descriptive verify anything you rely on):',
23
23
  '',
24
24
  ...(selfUrl ? [
25
- `0. YOUR ENDPOINT IS ${selfUrl} use exactly this base URL in any script or curl you write, never guess one and never use openzoo.fun (that is a website, not an API). Endpoints below are relative to it: ${selfUrl}/chat/completions, ${selfUrl}/hrr/bind, ${selfUrl}/models. Do NOT insert another /v1 this URL already ends in one.`,
25
+ `- Base URL: ${selfUrl}. It already ends in /v1, so routes are ${selfUrl}/chat/completions, ${selfUrl}/hrr/bind, ${selfUrl}/models. A doubled /v1 is repaired by the proxy but logs a warning. openzoo.fun is a website, not an API host.`,
26
26
  '',
27
27
  ] : []),
28
- '1. CONTEXT IS EFFECTIVELY UNBOUNDED. Any model here accepts corpora far past its own attention window: bodies over ~16KB are automatically carved and bound to a holographic (HRR) memory before the model sees them, and you then query against that. `context_length` in /v1/models is the client-usable ceiling (128M tokens), not the transformer window (that is `max_model_len`). DO NOT summarise, truncate, or "chunk to fit" a corpus to preserve context — send it whole and ask your question. One POST should stay under ~9.8M tokens (~32MiB) or the edge rejects it; for more than that, bind in several calls.',
28
+ '- Long bodies: requests over ~16KB are carved and bound to a holographic (HRR) memory before the model sees them, and the model answers from retrieval over that. So a large corpus can be sent whole; summarising or chunking it to fit is not required here, though nothing stops you. `context_length` in /v1/models reports the client-usable ceiling (128M tokens); the transformer window is `max_model_len`. A single POST over ~9.8M tokens (~32MiB) is rejected by the edge.',
29
29
  '',
30
- '2. THE BODY NEVER SHIPS TWICE. Put the corpus first, then a blank line, then your question. The corpus binds ONCE and every later question that reuses it ships only the question near-free, and much faster. Re-pasting the same corpus each turn wastes real money.',
30
+ '- Repeat sends: a corpus placed first, followed by a blank line and then a question, binds once. Later questions reusing it ship only the question, which is cheaper and faster. Re-sending the same corpus each turn costs full price each time.',
31
31
  '',
32
- ' EXPLICIT BIND (free, no payment): POST /v1/hrr/bind with {"corpus": "..."} returns {"context_id": "..."}. Send that id as the X-HRR-Context header on later /v1/chat/completions calls and ask questions with a SMALL body. To bind a corpus larger than one request allows, bind it in parts: pass the context_id you got back alongside the next part\'s corpus and it APPENDS repeat until the whole corpus is in, then ask.',
33
- ' Keep any single request under ~8MB. Bigger bodies are dropped by the network hop before they reach the proxy (an opaque 413 or a dead connection). This is a REQUEST size limit, not a context limit — the bound context can be far larger, which is what parts are for.',
34
- ' Paths: your base_url already ends in /v1, so post to {base_url}/hrr/bind — NOT {base_url}/v1/hrr/bind (that double /v1 404s; the proxy repairs it, but do not rely on that).',
32
+ ' Explicit bind (unpaid): POST /v1/hrr/bind with {"corpus": "..."} returns {"context_id": "..."}. Passing that id as the X-HRR-Context header on later /v1/chat/completions calls lets the body stay small. Passing an existing context_id alongside a new corpus appends to it, which is how a corpus larger than one request gets bound in parts.',
33
+ ' Request size: single requests over ~8MB are dropped by the network hop before reaching the proxy (opaque 413 or dead connection). That is a request limit, not a context limit.',
35
34
  '',
36
- '3. PAYMENT IS HANDLED. Every call is paid per-request from the operator\'s wallet via x402 (Solana / Base / Robinhood Chain, whichever is funded). There is no account and no rate limit to negotiate, and you never handle money. Never search the operator\'s machine for credentials GET / on this proxy describes it.',
37
- ' AUTH, precisely: /hrr/bind and GET /models need NO key, so a script you write can call them directly. Paid endpoints (/chat/completions) need the bearer key your client is already configured with — you cannot read that key, so DO NOT write a standalone script that calls a paid endpoint. Bind from a script if you like, then ask through this conversation, which is already authenticated.',
35
+ '- Payment: calls are settled per request from the operator\'s own wallet via x402 (Solana / Base / Robinhood Chain, whichever is funded). There is no account to create and no key for you to supply or handle. GET / on this proxy returns the same description. /hrr/bind and GET /models are unpaid; /chat/completions is paid and uses the bearer key the client is already configured with, which is not readable from inside the conversation.',
38
36
  '',
39
- '4. MODEL IDS ARE FORGIVING. Ask for any model id you like; unknown ids are matched to the nearest served model, and /v1/models lists what is real (each alias row carries `served_by`).',
37
+ '- Model ids: unknown ids are matched to the nearest served model rather than erroring. /v1/models lists what is actually served, and each alias row carries `served_by`.',
40
38
  ].join('\n');
41
39
 
42
40
  /** Back-compat: the briefing with no endpoint line. */
package/lib/proxy.js CHANGED
@@ -463,7 +463,15 @@ async function spillTranscript(body, log, req) {
463
463
  //
464
464
  // Claude Code identifies its session, so use that when it is offered and fall
465
465
  // back to the content anchor when it is not. Same memo, better key.
466
- const sessionId = req?.headers?.['x-session-id']
466
+ // CAPTURED FROM A LIVE claude-cli/2.1.232 REQUEST, not guessed. The first
467
+ // version of this checked x-session-id / x-claude-session-id /
468
+ // metadata.user_id — none of which Claude Code sends, so it silently fell
469
+ // back to the content anchor on every request and the feature did nothing.
470
+ // The real header list is:
471
+ // anthropic-beta, anthropic-version, x-app, x-claude-code-session-id,
472
+ // x-stainless-*
473
+ const sessionId = req?.headers?.['x-claude-code-session-id']
474
+ || req?.headers?.['x-session-id']
467
475
  || req?.headers?.['x-claude-session-id']
468
476
  || (typeof body?.metadata?.user_id === 'string' ? body.metadata.user_id : null);
469
477
  const anchor = sessionId ? `sid:${sessionId}` : corpus.slice(0, 2048);
@@ -499,9 +507,12 @@ async function spillTranscript(body, log, req) {
499
507
  spillMemo.set(anchor, { corpus, contextId: bind.contextId, hash: bind.hash });
500
508
  if (spillMemo.size > 32) spillMemo.delete(spillMemo.keys().next().value);
501
509
  const sent = msgs.length - cut;
510
+ // NAME THE KEY. A memo keyed on the wrong thing fails silently — it just
511
+ // re-binds forever and collides sessions — so the log says which key was used.
512
+ const keyKind = sessionId ? `sid ${String(sessionId).slice(0, 8)}` : 'content-anchor';
502
513
  log(bind.reused
503
- ? `transcript prefix already bound (${bind.contextId}) — sending ${sent}/${msgs.length} turns`
504
- : `transcript prefix bound (${mb(bind.bytes)}MB → ${bind.contextId}) — sending ${sent}/${msgs.length} turns`);
514
+ ? `transcript prefix already bound (${bind.contextId}, ${keyKind}) — sending ${sent}/${msgs.length} turns`
515
+ : `transcript prefix bound (${mb(bind.bytes)}MB → ${bind.contextId}, ${keyKind}) — sending ${sent}/${msgs.length} turns`);
505
516
 
506
517
  // ADAPTIVE TOP-K. A fixed 32 chunks is what was actually eating the saving:
507
518
  // MEASURED on a 56,265-token corpus, top_k 32 handed 9,990 tokens back and
@@ -1018,6 +1029,12 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
1018
1029
  if ((req.url || '').includes('/chat/completions') && req.method === 'POST') {
1019
1030
  servedRequests += 1;
1020
1031
  say(`\n<- request #${servedRequests} from ${(req.headers['user-agent'] || 'unknown').slice(0, 40)}`);
1032
+ // TEMPORARY: name the headers (not values) so we can see whether the
1033
+ // client offers a session id at all. Values are never logged — several
1034
+ // of these carry auth.
1035
+ if (process.env.OPENZOO_LOG_HEADERS === '1') {
1036
+ log(` headers: ${Object.keys(req.headers).sort().join(', ')}`);
1037
+ }
1021
1038
  }
1022
1039
  if (rewritablePath(req.method, req.url)) {
1023
1040
  const rw = await maybeRewriteModel(bodyBuf);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzoo",
3
- "version": "0.48.52",
3
+ "version": "0.48.54",
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",