cachegate 1.2.0 → 1.3.1

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/server.js CHANGED
@@ -35,6 +35,25 @@ const openaiProvider = require('./providers/openai');
35
35
  const failover = require('./failover');
36
36
 
37
37
  const app = express();
38
+ // Any deployment behind a reverse proxy or load balancer (nginx,
39
+ // Traefik, Render, Heroku, ...) forwards the real client IP in
40
+ // X-Forwarded-For rather than as the raw socket address. Express's own
41
+ // default (`trust proxy` unset, i.e. false) makes express-rate-limit
42
+ // refuse that header outright the moment it's present - it throws
43
+ // ERR_ERL_UNEXPECTED_X_FORWARDED_FOR inside its key generator on every
44
+ // request through a rate-limited route, rather than risk keying
45
+ // per-caller limits off a spoofable header it hasn't been told to
46
+ // trust. Found running this behind a single-hop proxy in production
47
+ // (Cachegate Cloud, 2026-09-04) - not fatal to the request itself, but
48
+ // it means the per-IP rate limiter was keying off the proxy's own IP
49
+ // for every caller instead of each real client, so brute-force/abuse
50
+ // limiting on auth-style routes was effectively shared across ALL
51
+ // users rather than per-user. `1` (trust exactly one hop) is the
52
+ // correct value for a single reverse-proxy topology - the common case
53
+ // this engine actually runs behind. A deployment with more than one
54
+ // proxy hop in front of it should set this to the real hop count
55
+ // instead (see Express's own `trust proxy` docs) rather than assume 1.
56
+ app.set('trust proxy', 1);
38
57
  // No X-Powered-By: Express - free, standard hardening (avoids handing a
39
58
  // public-facing service's framework fingerprint to every caller for no
40
59
  // benefit).
package/streaming.js CHANGED
@@ -1,77 +1,77 @@
1
- // model-router/streaming.js
2
- //
3
- // OpenAI-compatible SSE chunk framing, shared by both providers so
4
- // server.js has exactly one wire format to write regardless of which
5
- // provider actually answered - the provider adapters (chatStream()) do
6
- // their own event-format translation and hand server.js plain text
7
- // deltas plus a final usage/cost summary; this file turns that into the
8
- // bytes that go on the wire.
9
- //
10
- // Scope for this increment: PLAIN TEXT CONTENT ONLY. Tool-call
11
- // streaming (accumulating partial JSON arguments across chunks, one or
12
- // more calls in flight at once) is a genuinely harder, separate
13
- // problem - server.js rejects stream:true + tools with a clear error
14
- // rather than attempt a half-working version of it.
15
- //
16
- // The final chunk carries extra fields (cost_usd, provider, cached,
17
- // cache_type) beyond real OpenAI's wire format - the same deviation the
18
- // non-streaming JSON response already makes. This proxy is
19
- // OpenAI-COMPATIBLE in request/response SHAPE, not a byte-for-byte
20
- // clone of OpenAI's actual API; MemoCode's own callers need the cost
21
- // data, and no spec-compliant client chokes on unknown extra JSON
22
- // fields it doesn't look for.
23
-
24
- const crypto = require('crypto');
25
-
26
- function chunkFrame(payload) {
27
- return `data: ${JSON.stringify(payload)}\n\n`;
28
- }
29
-
30
- function doneFrame() {
31
- return 'data: [DONE]\n\n';
32
- }
33
-
34
- function genId() {
35
- return 'chatcmpl-' + crypto.randomBytes(12).toString('hex');
36
- }
37
-
38
- function baseChunk(id, model, choice) {
39
- return {
40
- id,
41
- object: 'chat.completion.chunk',
42
- created: Math.floor(Date.now() / 1000),
43
- model,
44
- choices: [choice]
45
- };
46
- }
47
-
48
- function roleChunk({ id, model }) {
49
- return chunkFrame(baseChunk(id, model, { index: 0, delta: { role: 'assistant' }, finish_reason: null }));
50
- }
51
-
52
- function deltaChunk({ id, model, content }) {
53
- return chunkFrame(baseChunk(id, model, { index: 0, delta: { content }, finish_reason: null }));
54
- }
55
-
56
- function finalChunk({ id, model, usage, cost_usd, provider, cached, cache_type }) {
57
- const frame = baseChunk(id, model, { index: 0, delta: {}, finish_reason: 'stop' });
58
- frame.usage = usage;
59
- frame.cost_usd = cost_usd;
60
- frame.provider = provider;
61
- frame.cached = !!cached;
62
- if (cache_type) frame.cache_type = cache_type;
63
- return chunkFrame(frame);
64
- }
65
-
66
- function errorFrame(message) {
67
- return chunkFrame({ error: { message } });
68
- }
69
-
70
- function startSse(res) {
71
- res.setHeader('Content-Type', 'text/event-stream');
72
- res.setHeader('Cache-Control', 'no-cache');
73
- res.setHeader('Connection', 'keep-alive');
74
- if (typeof res.flushHeaders === 'function') res.flushHeaders();
75
- }
76
-
77
- module.exports = { chunkFrame, doneFrame, genId, roleChunk, deltaChunk, finalChunk, errorFrame, startSse };
1
+ // model-router/streaming.js
2
+ //
3
+ // OpenAI-compatible SSE chunk framing, shared by both providers so
4
+ // server.js has exactly one wire format to write regardless of which
5
+ // provider actually answered - the provider adapters (chatStream()) do
6
+ // their own event-format translation and hand server.js plain text
7
+ // deltas plus a final usage/cost summary; this file turns that into the
8
+ // bytes that go on the wire.
9
+ //
10
+ // Scope for this increment: PLAIN TEXT CONTENT ONLY. Tool-call
11
+ // streaming (accumulating partial JSON arguments across chunks, one or
12
+ // more calls in flight at once) is a genuinely harder, separate
13
+ // problem - server.js rejects stream:true + tools with a clear error
14
+ // rather than attempt a half-working version of it.
15
+ //
16
+ // The final chunk carries extra fields (cost_usd, provider, cached,
17
+ // cache_type) beyond real OpenAI's wire format - the same deviation the
18
+ // non-streaming JSON response already makes. This proxy is
19
+ // OpenAI-COMPATIBLE in request/response SHAPE, not a byte-for-byte
20
+ // clone of OpenAI's actual API; MemoCode's own callers need the cost
21
+ // data, and no spec-compliant client chokes on unknown extra JSON
22
+ // fields it doesn't look for.
23
+
24
+ const crypto = require('crypto');
25
+
26
+ function chunkFrame(payload) {
27
+ return `data: ${JSON.stringify(payload)}\n\n`;
28
+ }
29
+
30
+ function doneFrame() {
31
+ return 'data: [DONE]\n\n';
32
+ }
33
+
34
+ function genId() {
35
+ return 'chatcmpl-' + crypto.randomBytes(12).toString('hex');
36
+ }
37
+
38
+ function baseChunk(id, model, choice) {
39
+ return {
40
+ id,
41
+ object: 'chat.completion.chunk',
42
+ created: Math.floor(Date.now() / 1000),
43
+ model,
44
+ choices: [choice]
45
+ };
46
+ }
47
+
48
+ function roleChunk({ id, model }) {
49
+ return chunkFrame(baseChunk(id, model, { index: 0, delta: { role: 'assistant' }, finish_reason: null }));
50
+ }
51
+
52
+ function deltaChunk({ id, model, content }) {
53
+ return chunkFrame(baseChunk(id, model, { index: 0, delta: { content }, finish_reason: null }));
54
+ }
55
+
56
+ function finalChunk({ id, model, usage, cost_usd, provider, cached, cache_type }) {
57
+ const frame = baseChunk(id, model, { index: 0, delta: {}, finish_reason: 'stop' });
58
+ frame.usage = usage;
59
+ frame.cost_usd = cost_usd;
60
+ frame.provider = provider;
61
+ frame.cached = !!cached;
62
+ if (cache_type) frame.cache_type = cache_type;
63
+ return chunkFrame(frame);
64
+ }
65
+
66
+ function errorFrame(message) {
67
+ return chunkFrame({ error: { message } });
68
+ }
69
+
70
+ function startSse(res) {
71
+ res.setHeader('Content-Type', 'text/event-stream');
72
+ res.setHeader('Cache-Control', 'no-cache');
73
+ res.setHeader('Connection', 'keep-alive');
74
+ if (typeof res.flushHeaders === 'function') res.flushHeaders();
75
+ }
76
+
77
+ module.exports = { chunkFrame, doneFrame, genId, roleChunk, deltaChunk, finalChunk, errorFrame, startSse };