cachegate 1.1.0 → 1.1.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/package.json +1 -1
- package/OPEN_SOURCE_ROADMAP.md +0 -855
- package/ROADMAP.md +0 -281
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cachegate",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Self-hostable, OpenAI-compatible LLM proxy: routes to the cheapest healthy provider, caches responses exactly and semantically, tracks cost and latency per call.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "server.js",
|
package/OPEN_SOURCE_ROADMAP.md
DELETED
|
@@ -1,855 +0,0 @@
|
|
|
1
|
-
# Open-Source Release Roadmap — 20 Steps
|
|
2
|
-
|
|
3
|
-
**What this is:** the concrete execution plan for `ROADMAP.md`'s Phase 5
|
|
4
|
-
("open-source release"). That phase has existed as a single line —
|
|
5
|
-
"Public GitHub repo → npm package → Docker image → announce" — since
|
|
6
|
-
2026-08-23; this is what actually has to happen to make each of those
|
|
7
|
-
four words true, broken into 20 numbered steps with the sub-work under
|
|
8
|
-
each, so progress is trackable the same way the embedded build itself
|
|
9
|
-
was.
|
|
10
|
-
|
|
11
|
-
**Scope, precisely** (see `ROADMAP.md` section 6 and the 2026-08-29
|
|
12
|
-
open-source-scope discussion in the Decision Journal): **only the
|
|
13
|
-
embedded engine gets open-sourced** — routing, caching, metrics,
|
|
14
|
-
providers, the dashboard. Login, payment, multi-tenancy, and anything
|
|
15
|
-
else that makes the **standalone** hosted product ($ Phase 6) are never
|
|
16
|
-
part of this repo, this package, or this Docker image. If a step below
|
|
17
|
-
ever seems to call for adding one of those things, that's a sign the
|
|
18
|
-
step has drifted out of scope, not a sign the scope needs expanding.
|
|
19
|
-
|
|
20
|
-
**Status legend:** ⬜ not started · 🟨 in progress · ✅ done · ⏸ blocked/waiting
|
|
21
|
-
|
|
22
|
-
**Lesson applied from the embedded build's own history (see
|
|
23
|
-
`ROADMAP.md`'s "Note on an older numbering"):** that 20-step list was
|
|
24
|
-
never written down as one document and step 18's content couldn't be
|
|
25
|
-
reconstructed months later. This one is written down as one document,
|
|
26
|
-
here, on purpose — update status inline as steps close, don't let this
|
|
27
|
-
become a second undocumented list.
|
|
28
|
-
|
|
29
|
-
---
|
|
30
|
-
|
|
31
|
-
## Group A — Decide before building anything (steps 1-5)
|
|
32
|
-
|
|
33
|
-
### 1. ✅ Decide the extraction method — **decided 2026-08-29: fresh, curated history**
|
|
34
|
-
- **Checked directly** (`git log --format="%an <%ae>" --all -- 210_apps/001_model_router/`):
|
|
35
|
-
the real commit history contains a real person's full name and
|
|
36
|
-
personal Gmail address in the author field, across 12+ commits — not
|
|
37
|
-
a hypothetical risk, a confirmed one. That alone settles the
|
|
38
|
-
decision: **do not preserve the original git history** when
|
|
39
|
-
extracting. A `git subtree split`/`filter-repo` approach would carry
|
|
40
|
-
that identity into a permanently public, unrescindable record the
|
|
41
|
-
moment the repo goes live.
|
|
42
|
-
- No leaked API keys or other secrets found in the history's actual
|
|
43
|
-
diffs (checked separately — see step 3) — the ONE real exposure is
|
|
44
|
-
the author metadata itself, which a fresh/squashed history sidesteps
|
|
45
|
-
entirely along with everything else.
|
|
46
|
-
- **Decision: fresh, curated history** — squash to a clean set of
|
|
47
|
-
milestone commits authored under a project/org identity, not
|
|
48
|
-
personal ones, when step 16 actually pushes this public.
|
|
49
|
-
|
|
50
|
-
### 2. ✅ Pick a license — **decided 2026-08-29: MIT**
|
|
51
|
-
- **Decision: MIT**, not a BSL-style source-available license, for this
|
|
52
|
-
first release. Reasoning, weighed against the alternative:
|
|
53
|
-
- Phase 5's whole stated purpose is trust/adoption ("top-of-funnel"
|
|
54
|
-
per section 3's own monetization table) - the two launch
|
|
55
|
-
communities already planned for (Hacker News, r/selfhosted) react
|
|
56
|
-
badly to source-available licenses on a first release from an
|
|
57
|
-
unknown project. BSL here risks damaging the exact thing Phase 5
|
|
58
|
-
exists to build.
|
|
59
|
-
- The competing-hosted-clone risk BSL would guard against is
|
|
60
|
-
theoretical today, not real: it requires someone to find this
|
|
61
|
-
project among much bigger established names (LiteLLM, Portkey,
|
|
62
|
-
OpenRouter), choose to build a hosted competitor on it instead of
|
|
63
|
-
those, and out-compete our own hosted tier on the same code with
|
|
64
|
-
none of our head start. Low-probability chain to defend against
|
|
65
|
-
before there's any traction worth defending.
|
|
66
|
-
- Real precedent (Elastic, MongoDB, Sentry) all started fully
|
|
67
|
-
permissive and only moved to a restrictive license YEARS later,
|
|
68
|
-
once a specific, material competitor was demonstrably siphoning
|
|
69
|
-
revenue - never before there was traction. Code already released
|
|
70
|
-
under MIT stays MIT forever, but nothing stops a FUTURE version
|
|
71
|
-
from adopting different terms later if a real competing clone
|
|
72
|
-
actually appears - so nothing is lost by not defending against a
|
|
73
|
-
threat that doesn't exist yet.
|
|
74
|
-
- MIT over Apache-2.0 specifically: LiteLLM, our closest named
|
|
75
|
-
competitor, already ships MIT - matching it lowers the evaluation
|
|
76
|
-
friction for the exact audience both projects are fishing from.
|
|
77
|
-
- Revisit ONLY if a real competing hosted clone materializes later -
|
|
78
|
-
that would apply to future versions/contributions, not retroactively.
|
|
79
|
-
- **Done:** `LICENSE` file added (MIT, "MemoCode" as copyright holder
|
|
80
|
-
as a placeholder - confirm the real legal entity name before step 15
|
|
81
|
-
actually makes the repo public).
|
|
82
|
-
|
|
83
|
-
### 3. ✅ Secrets and credential audit — **done 2026-08-29, clean**
|
|
84
|
-
- Grepped both the working tree AND the full commit history's diffs for
|
|
85
|
-
API key patterns (`sk-...`, `AIza...`, `xai-...`) and email addresses
|
|
86
|
-
— **no leaked keys found anywhere**, and the only email match was the
|
|
87
|
-
git author metadata already handled by step 1's decision, not
|
|
88
|
-
anything embedded in file content.
|
|
89
|
-
- `.env.example` confirmed to hold only placeholder values
|
|
90
|
-
(`sk-ant-api03-...` truncated, `your-random-internal-key`, etc.), no
|
|
91
|
-
real credentials.
|
|
92
|
-
- `MODEL_ROUTER_OWNER_EMAILS`-style references in the code/docs are the
|
|
93
|
-
env var NAME only, never a real value.
|
|
94
|
-
- Nothing to rotate. Re-run this same check once more right before
|
|
95
|
-
step 15 actually makes the repo public, as a final gate — a clean
|
|
96
|
-
result today doesn't exempt a later commit from the same scrutiny.
|
|
97
|
-
|
|
98
|
-
### 4. ✅ Strip MemoCode-specific coupling — **name decided 2026-08-29: cachegate**
|
|
99
|
-
- **Checked directly**: grepped all `.js` code (not docs) for
|
|
100
|
-
"memocode"/"MemoCode" — found only comments explaining context and
|
|
101
|
-
one branded startup log line (`🚀 MemoCode Model Router listening...`
|
|
102
|
-
in `server.js`), **zero functional coupling** — no hardcoded MemoCode
|
|
103
|
-
URLs, no assumption that only a MemoCode caller exists. The "drop-in
|
|
104
|
-
module" design claim holds up under inspection, not just assumption.
|
|
105
|
-
- **Naming — a real open decision, CEO's call (taste/brand, unlike the
|
|
106
|
-
license), not decided here.** Checked npm registry availability for
|
|
107
|
-
three neutral candidates: `cachegate` ✅ available, `routecache` ✅
|
|
108
|
-
available, `llm-routecache` ✅ available (`llm-relay` was tried first
|
|
109
|
-
and is already taken by an unrelated, similarly-scoped package).
|
|
110
|
-
- **Case for a neutral name** (not "MemoCode ___"): "MemoCode" is a
|
|
111
|
-
mnemonic/note-taking app name with no connection to "LLM gateway" —
|
|
112
|
-
a stranger evaluating self-hosted LLM routers on Hacker News or
|
|
113
|
-
r/selfhosted would find a "MemoCode Model Router" confusing (why
|
|
114
|
-
does a notes app also make infrastructure?), which works against
|
|
115
|
-
Phase 5's own adoption/trust goal. The eventual Phase 6 standalone
|
|
116
|
-
product will also want its own identity as an "OpenRouter/LiteLLM
|
|
117
|
-
competitor" (`ROADMAP.md` section 6's own framing) — easier to
|
|
118
|
-
establish that now than rename after people have already
|
|
119
|
-
starred/installed something.
|
|
120
|
-
- **Case for keeping MemoCode branding:** funnel value back to the
|
|
121
|
-
parent product; zero rename work.
|
|
122
|
-
- **Decided: `cachegate`.** Applied everywhere: `package.json`'s
|
|
123
|
-
`name`, `package-lock.json` regenerated, `README.md`'s title, and
|
|
124
|
-
the startup log line in `server.js` (was
|
|
125
|
-
`🚀 MemoCode Model Router listening...`, now `🚀 cachegate
|
|
126
|
-
listening...`). Confirmed clean afterward - grepped for any
|
|
127
|
-
remaining "memocode"/"MemoCode" reference in code or README, none
|
|
128
|
-
found. Full suite re-run after the rename: 96/96 passing.
|
|
129
|
-
|
|
130
|
-
### 5. ✅ Rewrite the README for a stranger, not a teammate — done 2026-08-29
|
|
131
|
-
- Existing `README.md` was already thorough and honest (features,
|
|
132
|
-
streaming caveats, dashboard limitations, all stated plainly) - most
|
|
133
|
-
of it needed no change. What was missing for an external reader:
|
|
134
|
-
- A "Why this instead of LiteLLM/Portkey/OpenRouter" section, pulling
|
|
135
|
-
in the honest positioning already drafted in `ROADMAP.md` section 2
|
|
136
|
-
(self-hosted-first, semantic cache with the LiteLLM caveat stated
|
|
137
|
-
plainly, Node.js-native, embeddable, the real 20-45%/47-90% savings
|
|
138
|
-
range instead of an inflated 86-95%).
|
|
139
|
-
- A "What this is NOT" section stating the scope boundary up front
|
|
140
|
-
(no hosted service, no login/billing/multi-tenancy, no 140-provider
|
|
141
|
-
reach, no vector-indexed semantic cache yet) - added, per this
|
|
142
|
-
doc's own step-14 principle, so nobody files an issue asking for
|
|
143
|
-
something never in scope.
|
|
144
|
-
- The quickstart assumed the MemoCode monorepo was already cloned
|
|
145
|
-
(`cd 210_apps/001_model_router` as step one) - now gives both a
|
|
146
|
-
standalone `git clone` path and an "embedded in your own app" path.
|
|
147
|
-
- One paragraph named MemoCode's own internal file
|
|
148
|
-
(`210_apps/000_backend/router-owner.mjs`) as the dashboard-gating
|
|
149
|
-
example - generalized to describe the PATTERN (front the dashboard
|
|
150
|
-
with your own app's login) without a path that won't exist in the
|
|
151
|
-
extracted repo.
|
|
152
|
-
- Title left as a placeholder pending step 4's naming decision.
|
|
153
|
-
|
|
154
|
-
---
|
|
155
|
-
|
|
156
|
-
## Group B — Make the code shippable outside this monorepo (steps 6-10)
|
|
157
|
-
|
|
158
|
-
### 6. 🟨 Package it as a real, standalone npm module — mostly done 2026-08-29
|
|
159
|
-
- `package.json`: `name` → `cachegate`, `description` rewritten to be
|
|
160
|
-
accurate standalone (no "for MemoCode" framing), `license: "MIT"`
|
|
161
|
-
added (matches step 2 - npm shows this on the package page without
|
|
162
|
-
needing to open `LICENSE`), `engines: {"node": ">=18.0.0"}` added.
|
|
163
|
-
Honest caveat: `>=18.0.0` is a reasonable floor given the
|
|
164
|
-
dependencies (Express 4, `redis` v4, `pg` v8 all support it), NOT a
|
|
165
|
-
claim this has been cross-version tested — only run against Node 22
|
|
166
|
-
in this sandbox.
|
|
167
|
-
- **`bin` entry added**: `"cachegate": "./server.js"`, plus a
|
|
168
|
-
`#!/usr/bin/env node` shebang line added to `server.js` (it had none)
|
|
169
|
-
and the file's execute bit set - both required for `npx cachegate` to
|
|
170
|
-
actually work once published (step 17). README's "Run it" section now
|
|
171
|
-
documents this as the zero-clone path.
|
|
172
|
-
- **Deferred on purpose, not forgotten:** the `0.1.0` → `1.0.0` semver
|
|
173
|
-
bump. Bumping it now, before the repo is actually public (step 16),
|
|
174
|
-
would sit oddly in the internal monorepo and communicate a stability
|
|
175
|
-
claim nothing has tested yet. Bump this at step 16/17, right when
|
|
176
|
-
it's actually true.
|
|
177
|
-
- Dependencies unchanged and already appropriate for a standalone
|
|
178
|
-
package - nothing in `package.json` assumed shared monorepo tooling
|
|
179
|
-
to begin with, verified by a fresh `npm install` + full test run in
|
|
180
|
-
an isolated worktree: 96/96 passing.
|
|
181
|
-
|
|
182
|
-
### 7. ✅ Standard OSS repo hygiene — done 2026-08-29
|
|
183
|
-
- `CONTRIBUTING.md` — local setup, how to run tests (including the two
|
|
184
|
-
real test-suite conventions a contributor needs to know: no test
|
|
185
|
-
calls a live provider, and metrics-writing tests need to isolate
|
|
186
|
-
their own state), and the scope boundary repeated up front (a PR
|
|
187
|
-
adding login/billing/multi-tenancy gets closed regardless of
|
|
188
|
-
quality — that's a separate product, not this engine).
|
|
189
|
-
- `CODE_OF_CONDUCT.md` — Contributor Covenant v2.1, adopted verbatim
|
|
190
|
-
(that's its actual intended use, not something to rewrite).
|
|
191
|
-
- `SECURITY.md` — points to GitHub's private vulnerability reporting
|
|
192
|
-
(Security tab → Report a vulnerability) rather than a fabricated
|
|
193
|
-
contact email; states concretely what counts as a security issue
|
|
194
|
-
for THIS project specifically (auth bypass, cross-deployment data
|
|
195
|
-
leak, a resource-exhaustion path rate limiting doesn't cover) versus
|
|
196
|
-
a regular bug. **Needs "Private vulnerability reporting" turned on**
|
|
197
|
-
in the repo's Settings once step 15 creates it — flagged inline in
|
|
198
|
-
the file itself as a maintainer note to remove once live.
|
|
199
|
-
- `.github/ISSUE_TEMPLATE/bug_report.md` + `feature_request.md` (the
|
|
200
|
-
feature template repeats the scope boundary up front too) +
|
|
201
|
-
`config.yml` (points to Security Advisories instead of a public
|
|
202
|
-
issue for vulnerabilities — has a placeholder `OWNER` in its URL,
|
|
203
|
-
flagged inline, to fill in at step 15).
|
|
204
|
-
- `.github/PULL_REQUEST_TEMPLATE.md` — a scope-boundary checkbox
|
|
205
|
-
reviewers can actually check against, plus what testing was done.
|
|
206
|
-
|
|
207
|
-
### 8. ✅ CI for the public repo — done 2026-08-29
|
|
208
|
-
- `.github/workflows/test.yml` added: runs on every push and PR
|
|
209
|
-
(deliberately NOT copying this monorepo's own root
|
|
210
|
-
`.github/workflows/e2e.yml`, which is `workflow_dispatch`-only — a
|
|
211
|
-
public OSS repo needs the automatic trigger, that's the whole point
|
|
212
|
-
of the trust signal this step exists for).
|
|
213
|
-
- **Real finding, not assumed:** `npm test` needs more than just Node.
|
|
214
|
-
`semanticCache.test.js` spawns its own throwaway `redis-server`
|
|
215
|
-
process directly (the binary isn't on `ubuntu-latest` by default —
|
|
216
|
-
added an install step); `metrics-postgres.test.js` needs a real
|
|
217
|
-
reachable Postgres. The workflow adds a Postgres service container
|
|
218
|
-
matching that test file's own default connection string exactly.
|
|
219
|
-
- **Second real finding, caught by actually stopping Postgres and
|
|
220
|
-
re-running the suite rather than trusting the code's own comment**:
|
|
221
|
-
`metrics-postgres.test.js`'s header comment claimed it "fails loudly
|
|
222
|
-
with ECONNREFUSED... rather than silently skipping" — checked
|
|
223
|
-
directly, and that's not what the code does. It probes connectivity
|
|
224
|
-
once and skips gracefully with a warning if Postgres isn't reachable,
|
|
225
|
-
and **every test still reports as passing either way** (96/96,
|
|
226
|
-
whether or not Postgres was running). Fixed the comment to describe
|
|
227
|
-
the real behavior, and noted the real consequence: without this
|
|
228
|
-
workflow's Postgres service, CI would report green while silently
|
|
229
|
-
never exercising that file at all — the service container isn't a
|
|
230
|
-
nice-to-have here, it's what makes the badge mean what it's supposed
|
|
231
|
-
to mean.
|
|
232
|
-
- Matrix across Node `18.x`/`20.x`/`22.x` — `package.json` declares
|
|
233
|
-
`engines: {"node": ">=18.0.0"}`; running against all three actually
|
|
234
|
-
backs that claim instead of leaving it asserted but untested.
|
|
235
|
-
- **Verified for real, not just written**: ran the actual suite locally
|
|
236
|
-
with both a live local Postgres (started, password/db created to
|
|
237
|
-
match the test's default string) and the pre-installed `redis-server`
|
|
238
|
-
binary present — 96/96 passing, ~11s (vs. ~1.3s when Postgres is
|
|
239
|
-
down and that file's tests silently skip) — confirms the difference
|
|
240
|
-
is real exercise, not a no-op.
|
|
241
|
-
- Added a CI-status badge to `README.md` (next to the license badge) —
|
|
242
|
-
has the same `OWNER` placeholder as `.github/ISSUE_TEMPLATE/config.yml`,
|
|
243
|
-
flagged inline, to fill in at step 15.
|
|
244
|
-
|
|
245
|
-
### 9. 🟨 Docker image — improved and documented; build NOT verified here, real limitation
|
|
246
|
-
- **Honest limitation, not glossed over**: this sandbox's network
|
|
247
|
-
egress policy blocks Docker Hub entirely (confirmed - `docker build`
|
|
248
|
-
fails pulling `node:20-slim` with a 403 policy denial from
|
|
249
|
-
`production.cloudfront.docker.com`, and the proxy status explicitly
|
|
250
|
-
lists it as a policy denial, not a transient failure worth retrying).
|
|
251
|
-
**The actual `docker build` + `docker run` + hit-`/health` verification
|
|
252
|
-
this step calls for could NOT be done from here** - that needs
|
|
253
|
-
whoever has real internet access (the CEO's local machine / DeepSeek's
|
|
254
|
-
lane) to run it for real before step 16 ships. Said plainly rather
|
|
255
|
-
than assumed to be fine.
|
|
256
|
-
- What WAS done, based on direct static review of the existing
|
|
257
|
-
`Dockerfile` (not a guess):
|
|
258
|
-
- **Added `.dockerignore`** - none existed. Without it, `COPY . .`
|
|
259
|
-
would copy a HOST-machine `node_modules` (if one happens to exist
|
|
260
|
-
at build time) straight over the image's own freshly-`npm ci`'d
|
|
261
|
-
one - a classic, easy-to-hit Docker footgun, especially likely
|
|
262
|
-
once this is a real repo other contributors build locally. Also
|
|
263
|
-
excludes `.env`, `.git`, `test/`, `.github/` - none of it belongs
|
|
264
|
-
in a runtime image.
|
|
265
|
-
- **Added a non-root `USER node`** - the official Node image already
|
|
266
|
-
ships this user (uid 1000); the Dockerfile just never used it,
|
|
267
|
-
running as root by default. Standard hardening for a public image.
|
|
268
|
-
- **Added a real `HEALTHCHECK`** - calls the existing `GET /health`
|
|
269
|
-
endpoint via Node's own `http` module (the slim base image has no
|
|
270
|
-
curl/wget), so `docker ps` actually reports `healthy`/`unhealthy`
|
|
271
|
-
instead of only "running."
|
|
272
|
-
- **Publish target decided: GitHub Container Registry (GHCR)**, not
|
|
273
|
-
Docker Hub - ties to the same repo/`GITHUB_TOKEN` already used for
|
|
274
|
-
CI with no separate account or credential to manage, and the image
|
|
275
|
-
inherits the repo's own visibility settings. Docker Hub has better
|
|
276
|
-
discoverability for someone specifically browsing Docker Hub's own
|
|
277
|
-
search, but the realistic audience here is someone already reading
|
|
278
|
-
this GitHub repo's README, not browsing Docker Hub cold - GHCR fits
|
|
279
|
-
that path better. Not closed off permanently: worth adding a Docker
|
|
280
|
-
Hub mirror later if search-driven discovery turns out to matter.
|
|
281
|
-
- README's "Run it" section gained a Docker subsection: `docker build`
|
|
282
|
-
+ `docker run` with `--env-file`, a note on what `HEALTHCHECK`
|
|
283
|
-
reports, and an explicit statement that Redis is NOT bundled in the
|
|
284
|
-
image (point `REDIS_URL` at an external instance; missing it disables
|
|
285
|
-
caching cleanly rather than failing to start).
|
|
286
|
-
- **Remaining, real, not done here:** the actual build-and-run
|
|
287
|
-
verification. Flagging this explicitly as an open item for whoever
|
|
288
|
-
picks this up next with real network access - do not treat this
|
|
289
|
-
step as fully closed until that verification actually happens.
|
|
290
|
-
|
|
291
|
-
### 10. ✅ Config and environment documentation — done 2026-08-29
|
|
292
|
-
- **Checked directly, not assumed**: grepped every `.js` file for
|
|
293
|
-
`process.env.` to build the actual, complete list of env vars the
|
|
294
|
-
code reads, then diffed it against `.env.example`.
|
|
295
|
-
- **Real finding**: `.env.example` (and the README's own `.env`
|
|
296
|
-
snippet) documented `ANTHROPIC_MODEL=claude-sonnet-4-5-20250929` as a
|
|
297
|
-
config value - **grepped for it and it's read NOWHERE in the code**.
|
|
298
|
-
The model is named per-request in the API call's own `model` field,
|
|
299
|
-
not configured via env at all. A new user setting this would see it
|
|
300
|
-
silently do nothing. Removed from both files, replaced with an
|
|
301
|
-
explicit note that the model is per-request, not env-configured.
|
|
302
|
-
- **Missing from `.env.example` entirely, now added**:
|
|
303
|
-
`ALLOW_INSECURE_LOCAL_DEV`, `EMBEDDING_MODEL` (which OpenAI embedding
|
|
304
|
-
model the semantic cache uses), `METRICS_LOG_PATH` (JSONL storage
|
|
305
|
-
location), `MEMOCODE_ROUTER_DATABASE_URL` (an alternate to
|
|
306
|
-
`DATABASE_URL` that takes priority when set - useful when embedding
|
|
307
|
-
this inside an app that already has its own `DATABASE_URL`), and
|
|
308
|
-
`ROUTER_TIERS_JSON` (override the default routing tiers).
|
|
309
|
-
- **Genuinely tested the quickstart, not just written it**: ran
|
|
310
|
-
`npm install`, copied `.env.example` to `.env`, filled in a test
|
|
311
|
-
internal key and a fake Anthropic key, ran `npm start` - boots clean,
|
|
312
|
-
logs "🚀 cachegate listening on port 4000." Hit `GET /health` (real
|
|
313
|
-
response, correctly reflecting config) and `POST
|
|
314
|
-
/v1/chat/completions` (reached Anthropic for real, got back a genuine
|
|
315
|
-
401 "API key is invalid" - proving the full pipeline works end to
|
|
316
|
-
end; only the placeholder key is fake, exactly what a real follower
|
|
317
|
-
would see before adding their own). Also confirmed `.env` and the
|
|
318
|
-
metrics `data/` directory stay gitignored, as claimed.
|
|
319
|
-
- README's `.env` snippet now points to `.env.example` for the full
|
|
320
|
-
option list instead of duplicating a partial, now-corrected copy of it.
|
|
321
|
-
|
|
322
|
-
---
|
|
323
|
-
|
|
324
|
-
## Group C — Keep embedded and public in sync, and safe (steps 11-14)
|
|
325
|
-
|
|
326
|
-
### 11. ✅ Decide the concrete sync mechanism — **decided 2026-08-29**
|
|
327
|
-
- `ROADMAP.md` section 6 states the *principle* ("wrap it, don't fork
|
|
328
|
-
it - same core logic, two thin deployment shells") but never names
|
|
329
|
-
an actual mechanism. Two directions were on the table:
|
|
330
|
-
- (a) the public repo becomes the source of truth, MemoCode vendors
|
|
331
|
-
it in via a script/subtree pull.
|
|
332
|
-
- (b) this monorepo directory stays the source of truth, a script
|
|
333
|
-
pushes/mirrors it out to the public repo on release.
|
|
334
|
-
- **Decided: (b) — this monorepo directory stays the source of truth.**
|
|
335
|
-
Reasoning:
|
|
336
|
-
- It matches reality, not a fresh ideal: every real day of
|
|
337
|
-
development on this router - the original 20-step build, the
|
|
338
|
-
failover feature, all ten OSS-prep steps so far - happened inside
|
|
339
|
-
this monorepo, through its own task-branch/PR/gate workflow
|
|
340
|
-
(`AGENTS.md`). Moving day-to-day development to the public repo
|
|
341
|
-
would mean either running two parallel workflows or abandoning the
|
|
342
|
-
one that's actually proven itself this week - neither is worth it
|
|
343
|
-
to satisfy a "public repo is canonical" ideal nobody needs yet.
|
|
344
|
-
- Option (a) would put every internal-only concern (this team's own
|
|
345
|
-
coordination-board conventions, anything MemoCode-embedding-
|
|
346
|
-
specific) through public PR review before it could land internally
|
|
347
|
-
- backwards for a team that needs to move fast on its own repo.
|
|
348
|
-
- This is also the well-established pattern for exactly this
|
|
349
|
-
situation, not a novel one: several real companies develop OSS
|
|
350
|
-
projects inside a private monorepo and mirror them out
|
|
351
|
-
(contributions flow back in via manual review + reapplication, not
|
|
352
|
-
automatic two-way merge) rather than developing directly in the
|
|
353
|
-
public repo.
|
|
354
|
-
- **The real gap this creates, named rather than ignored:** once
|
|
355
|
-
external contributors exist (post step 19's launch), their PRs land
|
|
356
|
-
on the PUBLIC repo first - there is no automatic path back into this
|
|
357
|
-
monorepo. Concrete resolution: an accepted external PR gets manually
|
|
358
|
-
reapplied to `210_apps/001_model_router/` as its own normal task
|
|
359
|
-
branch here (same `AGENTS.md` workflow as any other change - it goes
|
|
360
|
-
through this project's own gate even though it originated externally,
|
|
361
|
-
on purpose, not as an oversight: an external diff doesn't get to skip
|
|
362
|
-
this codebase's own verification standard just because GitHub already
|
|
363
|
-
approved it). This creates a real but bounded divergence window
|
|
364
|
-
between the two repos - acceptable, disclosed, and the discipline is
|
|
365
|
-
keeping that window short (reapply promptly), not eliminating it.
|
|
366
|
-
- **The actual repeatable command** (implemented for real in step 12,
|
|
367
|
-
sketched here so the decision isn't just prose): a single script,
|
|
368
|
-
`sync-oss-release.sh`, invoked as `./sync-oss-release.sh <path-to-public-repo-checkout>`,
|
|
369
|
-
that does, in order: (1) copy this directory's tracked files into the
|
|
370
|
-
target checkout, excluding anything `.gitignore`d; (2) run the same
|
|
371
|
-
secrets grep step 3 already established, failing loudly rather than
|
|
372
|
-
publishing on a hit; (3) bump `package.json`'s version per step 6's
|
|
373
|
-
deferred semver plan; (4) commit and leave the push to a human/CI
|
|
374
|
-
step, never auto-pushed. One command, not a remembered sequence of
|
|
375
|
-
manual copy-paste steps.
|
|
376
|
-
|
|
377
|
-
### 12. ✅ Build the actual sync script/workflow — done and genuinely tested 2026-08-29
|
|
378
|
-
- `sync-oss-release.sh` built per step 11's decision: mirrors this
|
|
379
|
-
directory's git-tracked files into a target checkout (removing
|
|
380
|
-
everything else there first, except its own `.git/`), scans for
|
|
381
|
-
secrets before touching anything, optionally bumps `package.json`'s
|
|
382
|
-
version (`--version X.Y.Z`), and commits in the TARGET repo without
|
|
383
|
-
pushing. Refuses to run against a path that isn't a git repository,
|
|
384
|
-
specifically so pointing it at the wrong path can't wipe something
|
|
385
|
-
unrelated.
|
|
386
|
-
- **Actually tested end-to-end, not just written** - a real target git
|
|
387
|
-
repo was created in scratch space and run through every real
|
|
388
|
-
scenario: a clean sync with `--version 0.9.0` (files copied
|
|
389
|
-
correctly, version bumped, a stale target-only file correctly
|
|
390
|
-
removed, real commit created); a resync with no version flag
|
|
391
|
-
(correctly left the version as whatever's currently in this
|
|
392
|
-
directory); an identical third run (correctly reported "nothing
|
|
393
|
-
changed," no empty commit); the non-git-directory safety guard
|
|
394
|
-
(correctly refused); and the secrets-scan abort path (a real fake
|
|
395
|
-
key was planted in `README.md` and the script was run against it).
|
|
396
|
-
- **Real bug found by that last test, not assumed to work**: the
|
|
397
|
-
secrets-scan regex FAILED to catch the planted key on the first
|
|
398
|
-
attempt. Root cause: the character class `[a-zA-Z0-9]{20,}` used
|
|
399
|
-
after `sk-`/`xai-` doesn't allow hyphens - and a real Anthropic key
|
|
400
|
-
looks like `sk-ant-api03-<random>`, where the hyphens immediately
|
|
401
|
-
after `sk-` broke the match after only 3 characters. Fixed to
|
|
402
|
-
`[a-zA-Z0-9_-]{20,}` (matching `AIza`'s pattern, which already had
|
|
403
|
-
this right), re-tested, and confirmed it now correctly aborts.
|
|
404
|
-
- **This is the exact same pattern used in steps 1, 3, and 8's manual
|
|
405
|
-
audits** - meaning those "clean" results were reached with the same
|
|
406
|
-
blind spot. Re-ran the full audit (working tree + entire commit
|
|
407
|
-
history) with the CORRECTED pattern before concluding anything:
|
|
408
|
-
still genuinely clean. The earlier conclusion holds, but only because
|
|
409
|
-
it was actually re-verified just now, not assumed to still be valid
|
|
410
|
-
once the flaw in the method was found.
|
|
411
|
-
- No separate "checklist for what needs re-verifying after a sync" was
|
|
412
|
-
needed beyond what the script itself already does (secrets scan is
|
|
413
|
-
automatic on every run) - a maintainer still reviews the target
|
|
414
|
-
repo's diff before pushing (the script's own final message says so),
|
|
415
|
-
which covers README drift and anything else worth a human glance.
|
|
416
|
-
|
|
417
|
-
### 13. ✅ Security review pass, specifically for "now public" risk — done 2026-08-29
|
|
418
|
-
- **The one real, serious finding, confirmed live not assumed**: sent
|
|
419
|
-
an actual oversized POST body during this review and got back a raw
|
|
420
|
-
HTML page containing a FULL STACK TRACE with this server's own
|
|
421
|
-
absolute filesystem paths - Express's own default error handler,
|
|
422
|
-
reached because nothing here ever caught an error raised before a
|
|
423
|
-
route's own try/catch (the concrete trigger: `express.json()`
|
|
424
|
-
rejecting a too-large body). Fixed with a catch-all JSON error
|
|
425
|
-
handler registered last, re-tested with the identical request:
|
|
426
|
-
`{"error":"Request body too large."}`, 413, no stack trace. This is
|
|
427
|
-
exactly the class of thing "worked fine as an internal tool nobody
|
|
428
|
-
attacked" and would not have survived being public.
|
|
429
|
-
- **Second real finding**: `express.json({ limit: '50mb' })` was
|
|
430
|
-
applied GLOBALLY and BEFORE the `/v1` auth check - an anonymous
|
|
431
|
-
caller could force up to 50MB of JSON parsing per request before
|
|
432
|
-
ever being rejected with 401. Fixed three ways: scoped to `/v1` only
|
|
433
|
-
(the sole route that reads a body - checked directly, every other
|
|
434
|
-
route is a body-less GET); moved after `requireInternalKey` and the
|
|
435
|
-
rate limiter in the middleware chain, so a request is rejected by a
|
|
436
|
-
cheap check before any parsing happens; limit dropped from 50mb to a
|
|
437
|
-
configurable 2mb default (`JSON_BODY_LIMIT`) - checked that this
|
|
438
|
-
router has no image/multimodal support, so even a very long text
|
|
439
|
-
conversation fits comfortably under that. Live-verified: an
|
|
440
|
-
unauthenticated 3MB POST now gets rejected in 16ms with a 401,
|
|
441
|
-
vs. previously being fully parsed first.
|
|
442
|
-
- **Third finding, lower severity**: `/stats` and `/dashboard/data`
|
|
443
|
-
had `requireInternalKey` but no rate limit at all, unlike `/v1`.
|
|
444
|
-
Added a separate, more generous `readEndpointLimiter`
|
|
445
|
-
(`READ_RATE_LIMIT_MAX`, default 120/window) - lower stakes than `/v1`
|
|
446
|
-
(no provider spend on the line) but still real server work a
|
|
447
|
-
leaked/shared key shouldn't be able to hammer unbounded.
|
|
448
|
-
- Free hardening added while in here: `app.disable('x-powered-by')` -
|
|
449
|
-
no reason to hand a public-facing service's framework fingerprint to
|
|
450
|
-
every caller.
|
|
451
|
-
- **Confirmed clean, no fix needed** (checked directly, not assumed):
|
|
452
|
-
no `.stack` or raw error object is ever sent to a client anywhere in
|
|
453
|
-
the existing route handlers - every error path already used
|
|
454
|
-
`err.message` only. `ALLOW_INSECURE_LOCAL_DEV` already refuses to
|
|
455
|
-
start without either a real key or this explicit opt-in, AND already
|
|
456
|
-
prints a loud runtime `console.warn` when active - no doc-only
|
|
457
|
-
warning to strengthen, the code itself already enforces it. No CORS
|
|
458
|
-
middleware exists, which is the CORRECT default here (an admin/data
|
|
459
|
-
API with bearer-token auth has no reason to allow arbitrary
|
|
460
|
-
cross-origin browser access) - not a gap to fix.
|
|
461
|
-
- Also clarified in `server.js`'s own rate-limiter comment: the
|
|
462
|
-
"shared ceiling across all callers" caveat is specific to a
|
|
463
|
-
single-caller EMBEDDED deployment - a standalone self-hoster with
|
|
464
|
-
distinct per-caller IPs gets real per-caller separation from the
|
|
465
|
-
same default, not the same limitation.
|
|
466
|
-
- All fixes verified live (server started, real HTTP requests sent and
|
|
467
|
-
checked) in addition to the automated suite - 96/96 passing
|
|
468
|
-
throughout, confirming none of this changed any existing behavior
|
|
469
|
-
other than the three things it was meant to fix.
|
|
470
|
-
|
|
471
|
-
### 14. ✅ Explicit non-goals, written down where a stranger will read them — done 2026-08-29
|
|
472
|
-
- Most of the literal content already existed from step 5 (the README's
|
|
473
|
-
"What this is NOT" section) and step 7 (`CONTRIBUTING.md`,
|
|
474
|
-
`.github/ISSUE_TEMPLATE/feature_request.md`) - checked all three
|
|
475
|
-
directly rather than assuming step 14 was redundant with them.
|
|
476
|
-
- **Real gap found in two of the three, not assumed fine**: both the
|
|
477
|
-
README's closing line and `CONTRIBUTING.md`'s scope section said, in
|
|
478
|
-
effect, "if you want a hosted service, fork it" - which actively
|
|
479
|
-
*invites* the exact outcome this section (and step 2's license
|
|
480
|
-
reasoning) exists to discourage. Technically true under MIT, but
|
|
481
|
-
saying it right next to "this is not a hosted service" undermines
|
|
482
|
-
the whole point of drawing the boundary. Fixed both: still honest
|
|
483
|
-
that MIT permits it (never claimed otherwise), but reframed as "not
|
|
484
|
-
a licensing restriction, a project-scope one" - this repo specifically
|
|
485
|
-
isn't going to grow into a hosted competitor to its own paid product,
|
|
486
|
-
full stop, rather than a soft invitation to go build one.
|
|
487
|
-
`.github/ISSUE_TEMPLATE/feature_request.md` already had this right -
|
|
488
|
-
checked, no change needed there.
|
|
489
|
-
- README's closing line also now explicitly separates the two real,
|
|
490
|
-
legitimate PR-worthy gaps (140-provider reach, vector-indexed
|
|
491
|
-
semantic cache) from the one boundary that isn't a gap at all,
|
|
492
|
-
pointing to `CONTRIBUTING.md` before someone opens a PR for it.
|
|
493
|
-
|
|
494
|
-
---
|
|
495
|
-
|
|
496
|
-
## Group D — Ship it (steps 15-18)
|
|
497
|
-
|
|
498
|
-
### 15. ✅ Create the real public GitHub repository — live 2026-08-29: https://github.com/iDebunk/cachegate
|
|
499
|
-
- **Owner decided: `iDebunk`** (the same GitHub account already used for
|
|
500
|
-
this monorepo's own repos - confirmed via `get_me` that it's a user
|
|
501
|
-
account, not a separate org, so "existing org" and "personal account"
|
|
502
|
-
were never actually two different choices here). Reasoning: matches
|
|
503
|
-
the common real-world pattern of hosting OSS directly under a
|
|
504
|
-
company's own account; a personal-account or brand-new-org
|
|
505
|
-
alternative would either tie a strategic asset to one individual or
|
|
506
|
-
invent a naming decision with no present need - and a repo can be
|
|
507
|
-
transferred to a different owner later at zero cost if that ever
|
|
508
|
-
changes, so this wasn't a one-way door.
|
|
509
|
-
- **Real limitation hit while executing this**: this session's own
|
|
510
|
-
`mcp__github__create_repository` call failed with a 403 ("Resource
|
|
511
|
-
not accessible by integration") - the connected GitHub App lacks
|
|
512
|
-
repository-creation scope, confirmed not a transient error. The CEO
|
|
513
|
-
created the repo manually via github.com/new instead (public, no
|
|
514
|
-
auto-init) - noted here so a future session doesn't waste a retry
|
|
515
|
-
loop on the same wall.
|
|
516
|
-
- **Real finding on branch protection's status-check picker**: the CI
|
|
517
|
-
workflow's `strategy: matrix` (Node 18.x/20.x/22.x, added in step 8)
|
|
518
|
-
means GitHub creates THREE separate status checks - `test (18.x)`,
|
|
519
|
-
`test (20.x)`, `test (22.x)` - not one called "Tests" as the
|
|
520
|
-
workflow's own top-level `name:` field might suggest. Searching
|
|
521
|
-
"Tests" in the branch-protection UI finds nothing; searching "test"
|
|
522
|
-
(the job id) finds all three. All three were required, matching the
|
|
523
|
-
actual reason the matrix exists (a single required check would let
|
|
524
|
-
the other two silently break without ever blocking a merge).
|
|
525
|
-
- **Also decided**: "Require branches to be up to date before merging"
|
|
526
|
-
left OFF for now - real value only once there's enough concurrent PR
|
|
527
|
-
traffic for it to matter, and it's a single checkbox to enable later
|
|
528
|
-
when that's true. "Require approvals: 1" kept ON, with admin bypass
|
|
529
|
-
intact (default), so external PRs need real review without locking
|
|
530
|
-
the maintainer out of merging their own work.
|
|
531
|
-
- Topics added via the repo's "About" gear (only appears once the repo
|
|
532
|
-
has content - see step 16): `llm-gateway`, `semantic-cache`,
|
|
533
|
-
`self-hosted`, `openai-compatible`, `llm-proxy`.
|
|
534
|
-
|
|
535
|
-
### 16. ✅ Push the extracted, cleaned codebase — done 2026-08-29
|
|
536
|
-
- Pushed from the CEO's own local machine using `sync-oss-release.sh`
|
|
537
|
-
(this session's own repo-scope restrictions block direct git push
|
|
538
|
-
from here too, same category of limitation as step 15's repo
|
|
539
|
-
creation) - the script itself worked exactly as tested: clean
|
|
540
|
-
secrets scan, version bumped to `1.0.0` (the real first-release
|
|
541
|
-
moment step 6's deferred plan was waiting for), 40 files mirrored,
|
|
542
|
-
one real commit.
|
|
543
|
-
- **Two real, Windows-specific findings, worth keeping for next time**:
|
|
544
|
-
(1) the script's shebang/`set -euo pipefail` line broke with
|
|
545
|
-
`$'\r': command not found` - Git on Windows had checked the file out
|
|
546
|
-
with CRLF line endings, which no bash interpreter handles in a
|
|
547
|
-
script. Fixed locally with `sed -i 's/\r$//' sync-oss-release.sh`
|
|
548
|
-
before running; added `.gitattributes` (forcing LF on `*.sh` and text
|
|
549
|
-
files generally) to this directory so a future Windows checkout gets
|
|
550
|
-
LF from git itself rather than needing the same manual `sed` fix
|
|
551
|
-
again. (2) `cp`-based file mirroring on
|
|
552
|
-
Windows/NTFS loses the Unix executable bit - `server.js` (needed for
|
|
553
|
-
its `npx cachegate` shebang) and `sync-oss-release.sh` itself both
|
|
554
|
-
landed as `644` in the initial commit. Fixed via
|
|
555
|
-
`git update-index --chmod=+x <file>` before pushing, which sets the
|
|
556
|
-
bit at the git level regardless of the filesystem - confirmed via
|
|
557
|
-
`git ls-files -s` showing `100755` before the push went out.
|
|
558
|
-
- First CI run on the real public repo passed for real (all three
|
|
559
|
-
Node-version matrix jobs green, ~1 minute) - genuine external
|
|
560
|
-
validation of step 8's workflow, not just this sandbox's own testing.
|
|
561
|
-
- Commit message rewritten before pushing to read as an actual first
|
|
562
|
-
release ("Initial public release of cachegate") rather than the
|
|
563
|
-
script's own generic ongoing-sync message, which is what every
|
|
564
|
-
*later* sync should look like, not this one-time moment.
|
|
565
|
-
|
|
566
|
-
### 17. ✅ Publish the npm package — live 2026-08-29: `npm install cachegate`
|
|
567
|
-
- Published from the CEO's own machine (this session has no npm
|
|
568
|
-
credentials, same category of limitation as steps 15-16's GitHub
|
|
569
|
-
access - noted once there, applies here too without repeating it).
|
|
570
|
-
- **Real wall hit**: npm's registry now requires either account 2FA or
|
|
571
|
-
a Granular Access Token with "Bypass two-factor authentication"
|
|
572
|
-
checked before it accepts a publish (403 otherwise) - npm's own
|
|
573
|
-
token-creation UI has also changed since older guides describe it
|
|
574
|
-
(no more separate "Automation" token type; it's a checkbox on the
|
|
575
|
-
one Granular Access Token form now, under Packages and scopes →
|
|
576
|
-
Permissions → Read and write → All packages, since an unpublished
|
|
577
|
-
package can't be individually selected yet).
|
|
578
|
-
- **A `"bin[cachegate]" script name was cleaned` warning appeared on
|
|
579
|
-
every publish attempt - checked directly rather than assumed
|
|
580
|
-
cosmetic**: queried the live registry after publishing
|
|
581
|
-
(`registry.npmjs.org/cachegate/1.0.0`) and confirmed the published
|
|
582
|
-
`bin` field is `{"cachegate":"server.js"}` - npm just stripped the
|
|
583
|
-
`./` prefix from `"./server.js"` as routine normalization. Not
|
|
584
|
-
corruption, not related to the earlier Windows CRLF finding from
|
|
585
|
-
step 16 - a real, if noisy, false alarm worth having actually
|
|
586
|
-
checked rather than left as an assumption either way.
|
|
587
|
-
- **Verified for real, not just published-and-assumed-fine**: installed
|
|
588
|
-
in a brand new, unrelated directory (`npm init -y` + `npm install
|
|
589
|
-
cachegate`) and confirmed `require('cachegate')` resolves and exposes
|
|
590
|
-
`{ app, isAuthConfigured }` - proves it installs and loads correctly
|
|
591
|
-
from the real registry, not just "works in the folder it was built
|
|
592
|
-
in."
|
|
593
|
-
- **Follow-up gap, caught by DeepSeek's read-only review and fixed
|
|
594
|
-
2026-08-29**: `sync-oss-release.sh --version X.Y.Z` bumps THIS
|
|
595
|
-
directory's own `package.json` (the monorepo source) in place before
|
|
596
|
-
copying, by design (see the script's own header comment) - but that
|
|
597
|
-
bump, from the original `1.0.0` publish, was never committed back
|
|
598
|
-
here. The monorepo's committed `package.json` sat at the stale
|
|
599
|
-
`0.1.0` while npm had `1.0.0` live, and the CEO's local
|
|
600
|
-
`memocode-fresh` checkout carried the real bump only as an
|
|
601
|
-
uncommitted working-tree change. Fixed by committing `package.json`
|
|
602
|
-
(and regenerating `package-lock.json` to match) at `1.0.0` here, so
|
|
603
|
-
the source of truth matches what's actually published instead of
|
|
604
|
-
lagging behind it silently.
|
|
605
|
-
|
|
606
|
-
### 18. ✅ Publish the Docker image — live 2026-08-29: `ghcr.io/idebunk/cachegate:latest`
|
|
607
|
-
- Pushed from the CEO's own machine (same credential/scope limitation
|
|
608
|
-
as steps 15-17, not repeated here). Built with `docker build`, tagged
|
|
609
|
-
both `:1.0.0` and `:latest`, pushed to GHCR with `docker login
|
|
610
|
-
ghcr.io` using a classic GitHub PAT (`write:packages` scope) as the
|
|
611
|
-
password prompt - a separate credential from the npm token in step
|
|
612
|
-
17. Real digest for both tags:
|
|
613
|
-
`sha256:c657784e882ceecd427974c4fb0b72a6953cf45da8fab66a3ae4cc4e2cd4e99f`.
|
|
614
|
-
- **Real wall hit**: GHCR images pushed under a personal account (not
|
|
615
|
-
an org) default to **Private** visibility even though the linked
|
|
616
|
-
repo is Public, and are not auto-linked to that repo's sidebar or
|
|
617
|
-
the pretty `/pkgs/container/<name>` URL (which 404s until linked).
|
|
618
|
-
Fixed the load-bearing part - visibility - via the package's own
|
|
619
|
-
Danger Zone (`github.com/<user>?tab=packages` → click the package
|
|
620
|
-
name → Package settings). The cosmetic repo-link
|
|
621
|
-
(`/pkgs/container/cachegate` resolving, sidebar showing the package)
|
|
622
|
-
was left as-is after confirming it has no effect on public
|
|
623
|
-
pullability - GitHub's package-listing UI showed inconsistent counts
|
|
624
|
-
between `?tab=packages` and a `?repo_name=` filtered variant of the
|
|
625
|
-
same page even after a refresh, which looks like a stale/broken
|
|
626
|
-
query-string filter on GitHub's own UI rather than a real state
|
|
627
|
-
problem, and wasn't worth chasing further.
|
|
628
|
-
- **Update 2026-08-30 - actually closed, not just deprioritized**: used
|
|
629
|
-
the package's own "Connect Repository" button (`Package settings` →
|
|
630
|
-
`Link this package to a repository`) and connected it to
|
|
631
|
-
`iDebunk/cachegate` specifically - worth naming plainly that the repo
|
|
632
|
-
picker also offered the private `2000_1010_memocode01` monorepo (same
|
|
633
|
-
owner, so GHCR's "must be the same owner" rule doesn't rule it out on
|
|
634
|
-
its own), which would have been a real mistake to pick, not just a
|
|
635
|
-
wrong label - a public package's page pointing at a private internal
|
|
636
|
-
repo defeats the entire point of the sync-out design. Verified after
|
|
637
|
-
connecting: the package page now shows `cachegate`'s actual README
|
|
638
|
-
content, confirming the correct repo, not the monorepo's.
|
|
639
|
-
- **Verified for real, cold, from a machine that has never built this
|
|
640
|
-
project**: `docker logout ghcr.io` (drops all local credentials),
|
|
641
|
-
then `docker rmi` both local tags, then `docker pull
|
|
642
|
-
ghcr.io/idebunk/cachegate:latest` - succeeded with zero credentials,
|
|
643
|
-
proving the image is genuinely public. First `docker run` +
|
|
644
|
-
`curl /health` attempt failed (`Exited (1)`, then later a timing-
|
|
645
|
-
related empty reply) - checked `docker logs` rather than guessing,
|
|
646
|
-
and found the real cause: the app's own security hardening from step
|
|
647
|
-
13 correctly refuses to start with an open `/v1` endpoint unless
|
|
648
|
-
`MODEL_ROUTER_INTERNAL_KEY` is set (working as designed, not a
|
|
649
|
-
packaging bug). Reran with that env var set, `docker ps` showed
|
|
650
|
-
`(healthy)`, and `curl http://localhost:4001/health` returned
|
|
651
|
-
`{"status":"healthy","redis_connected":false,...}` - a genuine cold
|
|
652
|
-
pull-run-verify cycle, not published-and-assumed-fine.
|
|
653
|
-
|
|
654
|
-
---
|
|
655
|
-
|
|
656
|
-
## Group E — Tell people, then keep it alive (steps 19-20)
|
|
657
|
-
|
|
658
|
-
### 19. ✅ Announce — posted live 2026-08-31
|
|
659
|
-
- All four posts drafted with real, distinct framing (see the original
|
|
660
|
-
drafting notes below), delivered as a published Artifact ("launch
|
|
661
|
-
kit") plus a standalone downloadable `.html` file.
|
|
662
|
-
- **Posted for real, live, from the CEO's own accounts** - the outward-
|
|
663
|
-
facing, hard-to-reverse action this session genuinely can't do
|
|
664
|
-
itself (no browser session or credentials on any of these
|
|
665
|
-
platforms). Confirmed done by the CEO directly; URLs weren't
|
|
666
|
-
collected/independently verified this pass, noted plainly rather
|
|
667
|
-
than implied otherwise.
|
|
668
|
-
- **HN title had to be trimmed live**: the original draft title (~98
|
|
669
|
-
chars) exceeded Hacker News's 80-character limit - shortened to
|
|
670
|
-
`Show HN: cachegate – self-hosted, cost-aware caching proxy for LLMs`
|
|
671
|
-
(67 chars) on the spot, same core pitch intact.
|
|
672
|
-
- **r/selfhosted was correctly skipped, not posted to**: its submit
|
|
673
|
-
flow required a mandatory flair, and one option was literally
|
|
674
|
-
"Release (No AI)" - cachegate was built with substantial AI
|
|
675
|
-
assistance (this whole roadmap's own execution is direct evidence),
|
|
676
|
-
so that flair genuinely didn't apply. Rather than force an
|
|
677
|
-
ill-fitting flair or imply something untrue, the CEO skipped that
|
|
678
|
-
specific subreddit outright - the right call, not a shortcut.
|
|
679
|
-
- **r/SelfHostedAI posted instead** - a different, more narrowly-
|
|
680
|
-
focused subreddit (self-hosted AI tooling specifically) with no such
|
|
681
|
-
flair conflict, reached mid-session while navigating Reddit's submit
|
|
682
|
-
flow. A reasonable, arguably better-targeted substitution for the
|
|
683
|
-
fourth platform, not the originally-planned r/selfhosted.
|
|
684
|
-
- **A near-miss caught before it mattered**: an early attempt landed on
|
|
685
|
-
`reddit.com/r/Art/submit` (a real subreddit for visual artwork, not
|
|
686
|
-
self-hosted software) - caught immediately from the page's own
|
|
687
|
-
"Title, Artist Name, Medium, Year" format requirement before
|
|
688
|
-
anything was submitted there.
|
|
689
|
-
- Final four actually posted: **Hacker News** (Show HN), **r/LocalLLaMA**,
|
|
690
|
-
**r/SelfHostedAI** (substituted for r/selfhosted), **Dev.to**.
|
|
691
|
-
|
|
692
|
-
Original drafting notes (why each framing was chosen): Show HN leads
|
|
693
|
-
with the honest savings range and the "what it doesn't do yet" list up
|
|
694
|
-
front (that crowd asks about gaps in the first three comments
|
|
695
|
-
regardless); r/LocalLLaMA leads with the one objection that sub raises
|
|
696
|
-
immediately - no local-model backend yet - framed as an open
|
|
697
|
-
contribution gap, not a hidden limitation; the self-hosted-targeted
|
|
698
|
-
post puts the `docker run` one-liner and the healthcheck/no-telemetry
|
|
699
|
-
details before any explanation; Dev.to is the one long-form piece with
|
|
700
|
-
the real origin story (an internal MemoCode cost-control tool that
|
|
701
|
-
turned out worth its own release) and the fullest explanation of the
|
|
702
|
-
exact/semantic cache distinction.
|
|
703
|
-
|
|
704
|
-
### 20. ✅ Post-launch triage plan — decided 2026-08-29
|
|
705
|
-
|
|
706
|
-
**Who watches, and how, stated plainly instead of assumed:**
|
|
707
|
-
`AGENTS.md`'s "one agent holds the standing watch" model (a Claude
|
|
708
|
-
session subscribed to every open PR via `subscribe_pr_activity`) is
|
|
709
|
-
what this team already uses internally - but it does not extend to
|
|
710
|
-
`cachegate` automatically. This session's own repo access is scoped to
|
|
711
|
-
a fixed list of repos for the environment it runs in, `cachegate`
|
|
712
|
-
is not on that list, and adding it hit a real, already-documented
|
|
713
|
-
limitation earlier in this same roadmap (`add_repo` on
|
|
714
|
-
`iDebunk/cachegate` failing with an unresolved approval prompt, back
|
|
715
|
-
in steps 15-16). So, honestly, for now:
|
|
716
|
-
- **The CEO is the standing watch**, via GitHub's own native "Watch →
|
|
717
|
-
All Activity" on the repo plus notifications, not a Claude session -
|
|
718
|
-
that's the real, working mechanism today, not an aspiration.
|
|
719
|
-
- **Response SLA**: first reply to any new issue or PR within 48
|
|
720
|
-
hours, even if just "looked at this, need more detail" or "queued,
|
|
721
|
-
will get to the actual fix by \<date\>" - a fast acknowledgment is
|
|
722
|
-
what prevents "reads as abandoned," not a fast fix.
|
|
723
|
-
- **Revisit if the environment's repo scope ever includes
|
|
724
|
-
`iDebunk/cachegate`** (an access-grant change outside this session's
|
|
725
|
-
own control, not something to keep retrying) - at that point, a
|
|
726
|
-
Claude session picking up the exact same standing-watch model used
|
|
727
|
-
internally becomes possible for real, and is the right thing to
|
|
728
|
-
switch to.
|
|
729
|
-
|
|
730
|
-
**"Phase 5 done" - one concrete bar, not a vibe:**
|
|
731
|
-
Phase 5 (this whole roadmap) is done when **either** of these is true,
|
|
732
|
-
whichever comes first, not both required:
|
|
733
|
-
- 4 consecutive weeks live with no open critical issue (a security
|
|
734
|
-
vulnerability, data loss, or the router silently returning wrong
|
|
735
|
-
answers) at any point during that window, **or**
|
|
736
|
-
- the first external (non-`iDebunk`) pull request is merged.
|
|
737
|
-
A quiet repo with zero critical bugs for a month is success on its own
|
|
738
|
-
terms; so is a total stranger trusting the code enough to send a real
|
|
739
|
-
patch before that clock runs out. Either is real evidence the release
|
|
740
|
-
worked - waiting for both would just be moving the goalpost.
|
|
741
|
-
|
|
742
|
-
**Phase 6 unlock:** per `ROADMAP.md`, Phase 6 (a standalone/hosted
|
|
743
|
-
product built on this same engine, explicitly NOT open-sourced itself)
|
|
744
|
-
becomes buildable once the bar above is actually met - not before, and
|
|
745
|
-
not automatically the moment it is either. Meeting the bar makes
|
|
746
|
-
Phase 6 *plannable* for real, using evidence from actual outside use
|
|
747
|
-
instead of internal dogfooding alone; starting to build it is still a
|
|
748
|
-
separate, deliberate decision when that time comes.
|
|
749
|
-
|
|
750
|
-
---
|
|
751
|
-
|
|
752
|
-
## Follow-ups discovered after the original 20 steps closed
|
|
753
|
-
|
|
754
|
-
Not renumbered into the list above - the original 20 are a closed,
|
|
755
|
-
honest historical record (see the scaffold-first rule in `AGENTS.md`).
|
|
756
|
-
New real work discovered later gets its own dated entry here instead.
|
|
757
|
-
|
|
758
|
-
### 21. ✅ Docker Hub mirror — live 2026-08-30: `docker.io/shipman/cachegate`
|
|
759
|
-
|
|
760
|
-
Step 9 originally decided GHCR over Docker Hub, explicitly leaving the
|
|
761
|
-
door open: *"Not closed off permanently: worth adding a Docker Hub
|
|
762
|
-
mirror later if search-driven discovery turns out to matter."* Revisited
|
|
763
|
-
because GHCR's own download counter turned out to be unreliable for
|
|
764
|
-
this purpose (checked directly - the live package page shows "Total
|
|
765
|
-
downloads: 0" even after step 18's own confirmed cold pull, so either
|
|
766
|
-
anonymous pulls aren't counted or there's a reporting lag; not
|
|
767
|
-
confirmed which) and Docker Hub remains a real, separate discovery
|
|
768
|
-
surface people specifically search.
|
|
769
|
-
|
|
770
|
-
- **Real wall hit, caught before it caused real confusion**: the plan
|
|
771
|
-
going in assumed the CEO's Docker Hub username was `cachegate`, matching
|
|
772
|
-
the project name. It isn't - `docker login` reported the real account
|
|
773
|
-
as `shipman`. First push to `cachegate/cachegate` correctly failed
|
|
774
|
-
(`insufficient_scope: authorization failed` - `shipman` has no write
|
|
775
|
-
access to a `cachegate` namespace). **Target corrected to
|
|
776
|
-
`docker.io/shipman/cachegate`** - Docker Hub auto-created the repo on
|
|
777
|
-
first push to that personal namespace, no separate creation step
|
|
778
|
-
needed.
|
|
779
|
-
- **Also fixed while in here**: added standard OCI labels to the
|
|
780
|
-
Dockerfile (`org.opencontainers.image.source`, `.description`,
|
|
781
|
-
`.licenses`). `image.source` specifically is what GHCR reads to link
|
|
782
|
-
a package to its repo automatically on every future push - the
|
|
783
|
-
durable alternative to clicking "Connect Repository" by hand once,
|
|
784
|
-
which doesn't survive a re-publish. Same three-line cost, fixes the
|
|
785
|
-
cosmetic repo-link gap left open back at step 18 for good. GHCR's
|
|
786
|
-
already-published `:1.0.0`/`:latest` tags were deliberately left
|
|
787
|
-
untouched - not worth rewriting an immutable version tag for a
|
|
788
|
-
cosmetic label; it reaches GHCR naturally on the next real version
|
|
789
|
-
bump.
|
|
790
|
-
- **Login was actually via Docker's device-code browser flow**, not a
|
|
791
|
-
pasted token at a password prompt - `docker login docker.io` opened
|
|
792
|
-
a one-time code + browser confirmation instead. Functionally
|
|
793
|
-
equivalent (an access token was still created and available as a
|
|
794
|
-
fallback), just a different, newer CLI flow than GHCR's or npm's.
|
|
795
|
-
- **Not a CI automation, on purpose, matching GHCR's own precedent**:
|
|
796
|
-
pushed manually from the CEO's own machine, same credential/scope
|
|
797
|
-
category as steps 15-18 - not something this session can do directly.
|
|
798
|
-
- **Verified for real, cold, same rigor as step 18**: `docker logout
|
|
799
|
-
docker.io` (drops all local credentials), `docker rmi` all local
|
|
800
|
-
tags, `docker pull docker.io/shipman/cachegate:latest` - succeeded
|
|
801
|
-
with zero credentials. Real digest:
|
|
802
|
-
`sha256:4d8548dda952ec1dceff6ff6a9c69e1c86d55193150395465f387c8da171a55e`.
|
|
803
|
-
First `curl /health` attempt hit the same timing gap seen at step 18
|
|
804
|
-
(container 1 second old, still `health: starting`) - not re-guessed
|
|
805
|
-
as a real bug this time, waited for `docker ps` to show `(healthy)`,
|
|
806
|
-
then `curl http://localhost:4002/health` returned
|
|
807
|
-
`{"status":"healthy","redis_connected":false,...}` - a genuine cold
|
|
808
|
-
pull-run-verify cycle on a second, independent registry.
|
|
809
|
-
|
|
810
|
-
### 22. 🟨 `--env-path` flag - built 2026-08-31, not yet republished
|
|
811
|
-
|
|
812
|
-
Decided after a design discussion with the CEO: the cwd-only `.env`
|
|
813
|
-
lookup (`npx cachegate` reads `.env` from wherever you happen to be
|
|
814
|
-
standing, not a fixed path) was a real, repeated point of confusion -
|
|
815
|
-
see step 21's own README work on "Wiring this into your app." Weighed
|
|
816
|
-
against a second candidate (per-app keys / usage tracked separately per
|
|
817
|
-
caller) and deliberately NOT building that one: the README already
|
|
818
|
-
states this is "a single-operator, self-hosted admin tool, not a
|
|
819
|
-
multi-tenant product," and real per-app key custody is explicitly
|
|
820
|
-
named in "What this is NOT" as reserved for the separate, closed,
|
|
821
|
-
hosted product this engine may one day sit under - building it into
|
|
822
|
-
the free engine now would cannibalize that product's own future
|
|
823
|
-
differentiation, not just cost more effort.
|
|
824
|
-
|
|
825
|
-
- **What was built**: an optional `--env-path <file>` /
|
|
826
|
-
`--env-path=<file>` flag, parsed from `process.argv` before
|
|
827
|
-
`dotenv.config()` runs. Backward compatible by construction - no flag
|
|
828
|
-
given behaves exactly as before (reads `.env` from the current
|
|
829
|
-
directory); flag given reads from wherever it points instead,
|
|
830
|
-
resolved relative to the invoking directory or absolute as given.
|
|
831
|
-
`resolveEnvPathFromArgv` extracted as its own small, pure, exported
|
|
832
|
-
function specifically so this could be unit-tested directly rather
|
|
833
|
-
than through a heavier process-spawning test.
|
|
834
|
-
- **Verified twice, not once**: 6 new unit tests
|
|
835
|
-
(`test/env-path.test.js`) covering both flag syntaxes
|
|
836
|
-
(`--env-path X` and `--env-path=X`), absolute paths, the flag missing
|
|
837
|
-
entirely, the flag present with no value after it, and other
|
|
838
|
-
unrelated flags not interfering. Full suite: 105/105 passing. Then a
|
|
839
|
-
real, live smoke test on top of the unit tests - not just trusting
|
|
840
|
-
them - starting `node server.js --env-path <path>` from a directory
|
|
841
|
-
with no `.env` at all, pointing at a real `.env` (with a distinct
|
|
842
|
-
`PORT=4099`) in a completely different location, and confirming via
|
|
843
|
-
`curl /health` that it actually started on the port from that other
|
|
844
|
-
file, not the default.
|
|
845
|
-
- **`README.md` updated** in both places that described the old
|
|
846
|
-
cwd-only behavior as absolute ("there's no separate config path for
|
|
847
|
-
this one") - both now correctly describe the flag as an option.
|
|
848
|
-
- **Version bumped to `1.1.0`** (`package.json` + regenerated
|
|
849
|
-
`package-lock.json`) - a real, backward-compatible feature addition,
|
|
850
|
-
not a patch.
|
|
851
|
-
- **Not done yet, on purpose**: this needs republishing to all three
|
|
852
|
-
registries (npm, GHCR, Docker Hub) before it's real for anyone
|
|
853
|
-
outside this monorepo, same cost and rigor as every other publish
|
|
854
|
-
step in this document - not marking this ✅ until that's actually
|
|
855
|
-
done and cold-verified again.
|
package/ROADMAP.md
DELETED
|
@@ -1,281 +0,0 @@
|
|
|
1
|
-
# MemoCode Model Router — Roadmap & Review
|
|
2
|
-
|
|
3
|
-
> Review of `210_apps/001_model_router/` as it stands, and the plan to take it
|
|
4
|
-
> from an internal MemoCode utility to a standalone self-hosted product.
|
|
5
|
-
> Originally written by DeepSeek, 2026-08-23, auditing the version that
|
|
6
|
-
> existed on `deepseek/memocode-chat-and-router` at the time. **Corrected the
|
|
7
|
-
> same day**: that audit's "gaps" section was accurate for the version it was
|
|
8
|
-
> looking at, but a second, much more complete implementation had been built
|
|
9
|
-
> in parallel on `claude/memocode-project-review-v0ohnx` and never merged
|
|
10
|
-
> anywhere shared — neither branch's author knew the other's existed. This
|
|
11
|
-
> file now reflects the merged, actual state. See section 0.
|
|
12
|
-
|
|
13
|
-
## 0. What happened, so it doesn't happen again
|
|
14
|
-
|
|
15
|
-
Two independent model-router implementations existed at once: this one
|
|
16
|
-
(minimal - proxy, exact-match cache, provider mapping) on
|
|
17
|
-
`deepseek/memocode-chat-and-router`, and a materially more complete one
|
|
18
|
-
(routing strategies, semantic cache, streaming, JSONL metrics with rotation,
|
|
19
|
-
a cost dashboard, hardened auth, 68 tests) built the same day on
|
|
20
|
-
`claude/memocode-project-review-v0ohnx` - a branch neither this file's
|
|
21
|
-
original audit nor DeepSeek's own semantic-cache attempt knew to check.
|
|
22
|
-
Discovered when the CEO asked to test the semantic cache and DeepSeek,
|
|
23
|
-
checking `master`/`deepseek/memocode-chat-and-router`/
|
|
24
|
-
`claude/session-import-feature`, correctly found it on none of them - but
|
|
25
|
-
hadn't checked the actual branch it lived on. Resolved by replacing this
|
|
26
|
-
directory's contents with the more complete version (a strict superset
|
|
27
|
-
except for this file) and keeping this file's own market/monetization
|
|
28
|
-
analysis, which stands independent of which codebase it's describing.
|
|
29
|
-
**The lesson, not just the fix**: a feature branch that never gets
|
|
30
|
-
merged toward `master` (or at least announced on the coordination board
|
|
31
|
-
with its branch name) is invisible to everyone not already looking at it,
|
|
32
|
-
no matter how complete the work on it is - completeness doesn't substitute
|
|
33
|
-
for discoverability.
|
|
34
|
-
|
|
35
|
-
## 1. What's actually here (audited 2026-08-23, post-merge)
|
|
36
|
-
|
|
37
|
-
**Working, tested, pushed** (`210_apps/001_model_router/`, this directory,
|
|
38
|
-
now matching `claude/memocode-project-review-v0ohnx`):
|
|
39
|
-
- `server.js` — Express, OpenAI-compatible `POST /v1/chat/completions`
|
|
40
|
-
(streaming and non-streaming), internal bearer-key auth (fails CLOSED by
|
|
41
|
-
default - `ALLOW_INSECURE_LOCAL_DEV=true` required to opt into running
|
|
42
|
-
without a key), rate limiting, `/health`, `/stats`, `/dashboard`.
|
|
43
|
-
- `providers/anthropic.js` + `providers/openai.js` — request mapping,
|
|
44
|
-
token/cost estimation, `chatStream()` for SSE.
|
|
45
|
-
- `cache.js` — Redis exact-match cache keyed by a content hash of
|
|
46
|
-
`{model, messages, temperature, max_tokens, tools, tool_choice}`.
|
|
47
|
-
- `semanticCache.js` + `embeddings.js` — near-duplicate matching via
|
|
48
|
-
embeddings + cosine similarity over a bounded per-model Redis list
|
|
49
|
-
(brute-force, not RediSearch/vector-indexed - an honest, documented
|
|
50
|
-
scale limit, not a hidden one). Tool-calling requests excluded. Semantic
|
|
51
|
-
hits tracked separately from exact hits in metrics, never blended into
|
|
52
|
-
one number.
|
|
53
|
-
- `router.js` — three explicit `router:` virtual-model strategies (cost /
|
|
54
|
-
latency / latency-guarded-cost), no invented blended score.
|
|
55
|
-
- `metrics.js` — JSONL logs with day-rotation, bounded reads, opt-in
|
|
56
|
-
pruning.
|
|
57
|
-
- `public/dashboard.html` — a real cost dashboard (KPI tiles, charts,
|
|
58
|
-
auto-refresh, table-view twins), built to this project's own `dataviz`
|
|
59
|
-
skill standard.
|
|
60
|
-
- 68 tests across 8 test files, all passing.
|
|
61
|
-
- Live: dogfooded by `210_apps/000_backend/chat-import-logic.mjs`
|
|
62
|
-
(`localhost:4000/v1/chat/completions` for chat segmentation).
|
|
63
|
-
|
|
64
|
-
**Real remaining gaps** (honest, not "nothing left to do"; updated
|
|
65
|
-
2026-08-29 after re-auditing against the actual code - two of the four
|
|
66
|
-
gaps below turned out to already be closed):
|
|
67
|
-
1. Semantic cache is brute-force cosine over a capped list, not a real
|
|
68
|
-
vector index - fine at self-hosted single-instance volume, not meant to
|
|
69
|
-
scale past `SEMANTIC_CACHE_MAX_CANDIDATES` (default 200) per model.
|
|
70
|
-
2. ~~No provider failover on a 5xx/rate-limit~~ **Closed 2026-08-29.**
|
|
71
|
-
`failover.js` + `server.js`'s non-streaming dispatch path now walk
|
|
72
|
-
`router.js`'s full ranked-candidate list, retrying the next candidate
|
|
73
|
-
when one fails for a reason that isn't the request's own fault (a
|
|
74
|
-
400/404 still fails immediately - retrying elsewhere wouldn't help).
|
|
75
|
-
Streaming is a deliberate exception, documented inline in
|
|
76
|
-
`handleStreamingDispatch` - SSE headers and the first frame commit to
|
|
77
|
-
a model name before a failure could be known, so silent mid-stream
|
|
78
|
-
provider switching is a materially harder problem, left open rather
|
|
79
|
-
than shipped half-working.
|
|
80
|
-
3. ~~No persistence layer under the dashboard~~ **Closed** (already true
|
|
81
|
-
before this pass, just never updated here): `metrics.js` has stored
|
|
82
|
-
metrics in Postgres since the `feat(model-router): persistent metrics
|
|
83
|
-
storage via Postgres` commit, with automatic JSONL fallback when no
|
|
84
|
-
database is configured.
|
|
85
|
-
4. Not yet open-sourced/packaged standalone (Phase 5, section 4 below).
|
|
86
|
-
|
|
87
|
-
**What this actually is today, said plainly**: working, tested internal
|
|
88
|
-
infrastructure with zero users outside this codebase — not an app, not a
|
|
89
|
-
published package, not a running public service, not something anyone has
|
|
90
|
-
paid for or even tried. Everything from here through section 3 (market
|
|
91
|
-
position, monetization tiers, MRR estimates) describes a *hypothetical*
|
|
92
|
-
product this code could become, not a claim about what it is right now.
|
|
93
|
-
Read it as a plan, not a status report - a CEO challenge on 2026-08-23
|
|
94
|
-
("this is not an app, it's not portable, we build nothing") is a fair
|
|
95
|
-
description of today's reality and is what section 4's Phase 5 (open
|
|
96
|
-
source release) exists to close.
|
|
97
|
-
|
|
98
|
-
## 2. Honest market position
|
|
99
|
-
|
|
100
|
-
Do **not** try to out-LiteLLM LiteLLM (140+ providers, Python, huge community)
|
|
101
|
-
or OpenRouter (acquired by Stripe). The winnable niche:
|
|
102
|
-
|
|
103
|
-
- **Self-hosted first** — prompts/PII never leave your infra.
|
|
104
|
-
- **Semantic cache**, not just exact-match (most OSS options still hash-match).
|
|
105
|
-
- **Node.js/TypeScript** — the JS/TS AI-app crowd is underserved.
|
|
106
|
-
- **Embeddable** — usable as a module inside an existing app before it's sold.
|
|
107
|
-
|
|
108
|
-
**Correction, same day, on the semantic-cache claim specifically**: verified
|
|
109
|
-
via WebSearch that LiteLLM shipped a real vector-indexed (Valkey-search +
|
|
110
|
-
HNSW) semantic cache in 2026 - more sophisticated than this router's
|
|
111
|
-
brute-force cosine scan. "Semantic cache, not just exact-match" is no longer
|
|
112
|
-
a differentiator against LiteLLM by name, even though it's still true against
|
|
113
|
-
"most OSS options." The honest remaining edges: small/auditable codebase,
|
|
114
|
-
Node-native, and already dogfooded inside a real app.
|
|
115
|
-
|
|
116
|
-
The sellable, honest claim (do not quote inflated 86–95%): **"Caching alone
|
|
117
|
-
typically saves 20–45%; add routing and it can reach 47–90% on well-tuned
|
|
118
|
-
traffic."** Being the vendor that quotes the real number is the credibility
|
|
119
|
-
edge with the buyer who has been burned by an inflated claim.
|
|
120
|
-
|
|
121
|
-
## 3. How this router makes money (open-core, honest)
|
|
122
|
-
|
|
123
|
-
Self-hosted OSS infrastructure monetizes by **open-core**, not by charging for
|
|
124
|
-
the free thing:
|
|
125
|
-
|
|
126
|
-
| Tier | What | Price | Why someone pays |
|
|
127
|
-
|---|---|---|---|
|
|
128
|
-
| **OSS core (free)** | exact-match cache, 2–3 providers, basic proxy | $0 | top-of-funnel; builds trust |
|
|
129
|
-
| **Pro** | semantic cache, cost-based routing + failover, cost dashboard | $49–99/mo | the "save 40–80% on LLM spend" features; charge ~10–20% of the savings |
|
|
130
|
-
| **Team** | multi-user, SSO, audit log, priority support | $199–499/mo | small teams running production workloads |
|
|
131
|
-
| **Hosted** | we run it for you (no ops) | usage-based, $99–999/mo | teams that don't want to self-host (where LiteLLM makes its money) |
|
|
132
|
-
| **Enterprise** | on-prem, compliance, SLAs | custom | regulated buyers |
|
|
133
|
-
|
|
134
|
-
**Honest caveats:** OSS → paid conversion is typically 1–5%; the money is in
|
|
135
|
-
the small fraction who want the semantic cache + dashboard and don't want to
|
|
136
|
-
self-host. It is a crowded market. Realistic: **$5–20K MRR in 12 months** by
|
|
137
|
-
owning the self-hosted Node.js niche. The metric that makes it work: a buyer
|
|
138
|
-
spending $1,000/mo on LLM APIs saves $400–800/mo with this — paying $50–100/mo
|
|
139
|
-
for the tool is an easy yes.
|
|
140
|
-
|
|
141
|
-
## 4. Phases
|
|
142
|
-
|
|
143
|
-
**Note on an older numbering (added 2026-08-29):** early journal entries
|
|
144
|
-
(2026-08-24) refer to this work as a flat "20-step roadmap" (steps
|
|
145
|
-
13-17 individually named there; 18 marked done but never described; 19
|
|
146
|
-
= the semantic-cache vector-index upgrade, paused; 20 = the standalone
|
|
147
|
-
product). That numbering was never written down as one document - it
|
|
148
|
-
only ever existed as scattered journal references, which made it
|
|
149
|
-
genuinely hard to reconstruct later (confirmed 2026-08-29: step 18's
|
|
150
|
-
actual content couldn't be found anywhere). **The 6 phases below
|
|
151
|
-
supersede that numbering entirely.** If an old "step N" reference ever
|
|
152
|
-
surfaces again, map it here rather than trying to revive the flat list.
|
|
153
|
-
|
|
154
|
-
### Phase 1 — make what exists real — **done**
|
|
155
|
-
Redis verified end-to-end, honest README, tests (68, not the "no test files"
|
|
156
|
-
this section originally reported), the honest cost claim above.
|
|
157
|
-
|
|
158
|
-
### Phase 2 — semantic cache — **done**
|
|
159
|
-
Embeddings + cosine similarity, `SEMANTIC_CACHE_THRESHOLD` default 0.93 (this
|
|
160
|
-
section's own original draft proposed ~0.95 - close, tuned during real
|
|
161
|
-
testing; see `semanticCache.js`'s own comment for the reasoning), tool-calls
|
|
162
|
-
excluded, tracked separately from exact-match hits.
|
|
163
|
-
|
|
164
|
-
### Phase 3 — real routing — **done**
|
|
165
|
-
Three explicit strategies (`router.js`) rather than a single invented
|
|
166
|
-
blended score - cost-ascending, latency-then-cost, and latency-guarded-cost
|
|
167
|
-
(excludes anything too much slower than the fastest known candidate).
|
|
168
|
-
Failover on error for non-streaming requests shipped 2026-08-29
|
|
169
|
-
(`failover.js`) - see section 1, gap 2, for what it does and doesn't cover.
|
|
170
|
-
|
|
171
|
-
### Phase 4 — cost visibility — **done**
|
|
172
|
-
`metrics.js` (JSONL, day-rotation, bounded reads) + `public/dashboard.html`
|
|
173
|
-
(KPI tiles, charts, auto-refresh, table-view twins).
|
|
174
|
-
|
|
175
|
-
### Phase 5 — open-source release — not started
|
|
176
|
-
Public GitHub repo → npm package → Docker image → announce (Hacker News,
|
|
177
|
-
r/LocalLLaMA, r/selfhosted, Dev.to). Still the real next milestone - none of
|
|
178
|
-
phases 1-4 being done changes that this hasn't shipped to anyone outside
|
|
179
|
-
this project yet. **Execution plan, added 2026-08-29:**
|
|
180
|
-
[`OPEN_SOURCE_ROADMAP.md`](./OPEN_SOURCE_ROADMAP.md) - the 20-step
|
|
181
|
-
breakdown of everything this one-liner actually requires, written down
|
|
182
|
-
as one document on purpose (see that file's own note on why).
|
|
183
|
-
|
|
184
|
-
### Phase 6 — hosted tier (only after OSS traction)
|
|
185
|
-
Render/Fly hosted; usage-based pricing.
|
|
186
|
-
|
|
187
|
-
## 5. One honest caveat on dogfooding
|
|
188
|
-
|
|
189
|
-
Chat-import's one segmentation call per session is too low-repeat to prove
|
|
190
|
-
the *cache* saves money — it only proves the *proxy* works. **2026-08-23
|
|
191
|
-
update**: `210_apps/000_backend/ai-providers.mjs`'s "Generate with AI" now
|
|
192
|
-
routes through this router too (app's own shared key only, never a
|
|
193
|
-
signed-in user's BYOK key — the router is one shared-secret proxy today,
|
|
194
|
-
not multi-tenant).
|
|
195
|
-
|
|
196
|
-
**Correction, same day, on how that update was first worded here**: this
|
|
197
|
-
section originally called that traffic "a genuinely more promising source
|
|
198
|
-
of real cache hits" — that was a hunch stated as a finding, and a CEO
|
|
199
|
-
challenge caught it (fair: "what are the odds two people ask the same
|
|
200
|
-
thing" is the right question, and for this app's actual traffic the honest
|
|
201
|
-
answer is "mostly low, and nobody has measured it"). The one real
|
|
202
|
-
structural fact in favor of *some* hits: `ai-providers.mjs` builds that
|
|
203
|
-
prompt as `"Subject: <topic>. Generate about N <label> worth of
|
|
204
|
-
content."` — short and templated, with `topic` the only variable — so two
|
|
205
|
-
different users both studying, say, "the Roman Empire" would produce an
|
|
206
|
-
identical or near-identical request, which chat-import's full conversation
|
|
207
|
-
transcripts basically never do. That's a plausible *mechanism* for a
|
|
208
|
-
non-zero hit rate on a study app whose users cluster around common
|
|
209
|
-
curriculum topics — it is not a measured hit rate, and treating it as one
|
|
210
|
-
would repeat the same mistake. Nobody has looked at MemoCode's actual
|
|
211
|
-
topic distribution. `MODEL_ROUTER_URL` is also still unset in the current
|
|
212
|
-
Render deploy (`render.yaml` declares no router service), so none of this
|
|
213
|
-
runs in production yet regardless.
|
|
214
|
-
|
|
215
|
-
To validate savings for real, you need either that traffic running through
|
|
216
|
-
the router in production with real measurement, or synthetic load testing
|
|
217
|
-
against a realistic topic distribution — not a plausible-sounding argument
|
|
218
|
-
for why it might work, however structurally reasonable that argument is.
|
|
219
|
-
|
|
220
|
-
## 6. Two-version architecture (decision 2026-08-23)
|
|
221
|
-
|
|
222
|
-
One engine (routing + cache + metrics), two wrappers — NOT two codebases:
|
|
223
|
-
|
|
224
|
-
| | Embedded | Standalone |
|
|
225
|
-
|---|---|---|
|
|
226
|
-
| Lives | inside MemoCode (own process) | its own hosted service (Render) |
|
|
227
|
-
| Keys | you bring Anthropic/OpenAI | it holds keys, issues its own key + URL |
|
|
228
|
-
| Login/billing | none | yes (login, payment, multi-tenancy) |
|
|
229
|
-
| Serves | your apps (fallback) | everybody, including your apps |
|
|
230
|
-
| Status | done — needs Redis + production wiring | future project (OpenRouter/LiteLLM competitor) |
|
|
231
|
-
|
|
232
|
-
Fallback: apps point at the standalone and drop back to the embedded
|
|
233
|
-
(localhost or MemoCode's own instance) when the standalone is down. Same job,
|
|
234
|
-
so the switch is invisible to the caller. Do NOT fork the router — wrap it:
|
|
235
|
-
same core logic, two thin deployment shells, so the two can never drift apart
|
|
236
|
-
the way the two parallel implementations did (section 0).
|
|
237
|
-
|
|
238
|
-
The embedded version **stays embedded in each new app** (each app inherits
|
|
239
|
-
this directory as-is) — it is NOT extracted to a shared repo. A shared repo
|
|
240
|
-
means maintaining one router copy per repo plus separate hosting/db, which
|
|
241
|
-
isn't worth it; a new app just copies `210_apps/001_model_router/` into its
|
|
242
|
-
own backend and points `MODEL_ROUTER_URL` at it. The standalone's key+URL are
|
|
243
|
-
left as comments in the app's `.env` as a future reminder, not wired yet.
|
|
244
|
-
|
|
245
|
-
Sequencing: finish the embedded first (cheap, real, no risk), then decide
|
|
246
|
-
whether to build the standalone's billing layer (the hard, deferred part).
|
|
247
|
-
|
|
248
|
-
## 7. Embedded — production checklist
|
|
249
|
-
|
|
250
|
-
1. Hosted Redis (Render Key Value) → set `REDIS_URL`.
|
|
251
|
-
2. Deploy this directory as its own service (or co-locate with the backend).
|
|
252
|
-
The repo `render.yaml` now declares a `memocode-router` service (Node
|
|
253
|
-
runtime, `npm ci` + `npm start`) and a `Dockerfile` is included for
|
|
254
|
-
Docker-based deploys.
|
|
255
|
-
3. Set production env: `MODEL_ROUTER_INTERNAL_KEY` (strong, `openssl rand -hex 32`),
|
|
256
|
-
`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `REDIS_URL`,
|
|
257
|
-
`SEMANTIC_CACHE_ENABLED=false` (low-repeat traffic: semantic cache spends
|
|
258
|
-
an embedding call on every miss for nothing), `ROUTER_STRATEGY=cost`.
|
|
259
|
-
4. Route MemoCode's AI call sites through it via `MODEL_ROUTER_URL`.
|
|
260
|
-
**Corrected 2026-08-29**: this item previously named "image gen" and
|
|
261
|
-
"transcription" as call sites to route through the router - checked
|
|
262
|
-
directly, and that's not actually possible as scoped. The router is an
|
|
263
|
-
OpenAI-*chat-completions*-compatible proxy only; image generation
|
|
264
|
-
(`images.generate`) and Whisper transcription
|
|
265
|
-
(`audio.transcriptions.create`) are different API shapes it doesn't
|
|
266
|
-
speak. What's actually wired today: chat-import's segmentation call
|
|
267
|
-
(`chat-import-logic.mjs`) and "Generate with AI" text generation
|
|
268
|
-
(`ai-providers.mjs`'s `generateStructuredText`), both gated behind
|
|
269
|
-
`MODEL_ROUTER_URL` being set (`shouldRouteToModelRouter()`). Extending
|
|
270
|
-
the router to proxy image/audio calls too would be new scope, not a
|
|
271
|
-
pending item on this checklist.
|
|
272
|
-
5. Verify `/health`, `/dashboard`, `/stats` on production with real traffic.
|
|
273
|
-
|
|
274
|
-
**Note on items 1 and 3 above (2026-08-29):** `render.yaml` declares the
|
|
275
|
-
env var slots for `REDIS_URL`, `MODEL_ROUTER_INTERNAL_KEY`,
|
|
276
|
-
`ANTHROPIC_API_KEY`, and `OPENAI_API_KEY` (all `sync: false` - filled in
|
|
277
|
-
manually on Render's dashboard), and the metrics Postgres connection is
|
|
278
|
-
wired automatically via `fromDatabase`. Whether those manual slots are
|
|
279
|
-
actually populated with real values on the live Render service isn't
|
|
280
|
-
something a sandboxed session can check - that verification needs
|
|
281
|
-
whoever has the Render dashboard.
|