cachegate 1.1.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +127 -112
- package/README.md +31 -13
- package/cache.js +72 -51
- package/embeddings.js +42 -32
- package/metrics.js +609 -556
- package/package.json +15 -1
- package/redisClient.js +55 -45
- package/router.js +254 -218
- package/semanticCache.js +159 -154
- package/server.js +282 -113
- package/.dockerignore +0 -11
- package/.gitattributes +0 -12
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -33
- package/.github/ISSUE_TEMPLATE/config.yml +0 -5
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -29
- package/.github/PULL_REQUEST_TEMPLATE.md +0 -25
- package/.github/workflows/test.yml +0 -63
- package/CODE_OF_CONDUCT.md +0 -66
- package/CONTRIBUTING.md +0 -94
- package/Dockerfile +0 -24
- package/SECURITY.md +0 -39
- package/sync-oss-release.sh +0 -160
- package/test/auth-config.test.js +0 -27
- package/test/cache.test.js +0 -33
- package/test/embeddings.test.js +0 -24
- package/test/env-path.test.js +0 -41
- package/test/failover.test.js +0 -99
- package/test/metrics-postgres.test.js +0 -183
- package/test/metrics.test.js +0 -282
- package/test/router.test.js +0 -195
- package/test/semanticCache.test.js +0 -167
- package/test/server.test.js +0 -357
- package/test/streaming.test.js +0 -248
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: Bug report
|
|
3
|
-
about: Something isn't working the way it should
|
|
4
|
-
title: ''
|
|
5
|
-
labels: bug
|
|
6
|
-
assignees: ''
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
**What happened**
|
|
10
|
-
A clear description of the actual behavior.
|
|
11
|
-
|
|
12
|
-
**What you expected instead**
|
|
13
|
-
|
|
14
|
-
**Steps to reproduce**
|
|
15
|
-
1.
|
|
16
|
-
2.
|
|
17
|
-
3.
|
|
18
|
-
|
|
19
|
-
**Environment**
|
|
20
|
-
- cachegate version:
|
|
21
|
-
- Node version:
|
|
22
|
-
- Deployment: standalone / embedded in another app / Docker
|
|
23
|
-
- Redis: version, and whether it's configured at all (some features
|
|
24
|
-
degrade cleanly without it — see README's "Features")
|
|
25
|
-
|
|
26
|
-
**Relevant config**
|
|
27
|
-
Which env vars are set (names only — **never paste real API keys, the
|
|
28
|
-
internal key, or a database URL here**): e.g. `ROUTER_STRATEGY=cost`,
|
|
29
|
-
`SEMANTIC_CACHE_ENABLED=true`, `DATABASE_URL` set (Postgres) vs. unset
|
|
30
|
-
(JSONL).
|
|
31
|
-
|
|
32
|
-
**Logs**
|
|
33
|
-
Relevant server console output, with any secret values redacted.
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: Feature request
|
|
3
|
-
about: Suggest something this project should do
|
|
4
|
-
title: ''
|
|
5
|
-
labels: enhancement
|
|
6
|
-
assignees: ''
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
**Before filing — is this in scope?**
|
|
10
|
-
Read `README.md`'s "What this is NOT" section first. Login, billing,
|
|
11
|
-
multi-tenant key custody, or a hosted-service feature will be closed as
|
|
12
|
-
out of scope regardless of how it's proposed — that functionality
|
|
13
|
-
belongs to a separate closed product built on this engine, not to this
|
|
14
|
-
engine. Everything else (routing, caching, provider support, the
|
|
15
|
-
dashboard, operational tooling) is fair game.
|
|
16
|
-
|
|
17
|
-
**What problem does this solve?**
|
|
18
|
-
Describe the actual pain, not just the feature - "I need X because Y
|
|
19
|
-
currently forces me to Z."
|
|
20
|
-
|
|
21
|
-
**What would the ideal solution look like?**
|
|
22
|
-
A sketch is fine - an API shape, a new env var, a CLI flag.
|
|
23
|
-
|
|
24
|
-
**Alternatives considered**
|
|
25
|
-
What are you doing today without this? Is there a workaround?
|
|
26
|
-
|
|
27
|
-
**Additional context**
|
|
28
|
-
Anything else - a link to how a similar tool does this, a real
|
|
29
|
-
production scenario this came up in.
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
## What changed and why
|
|
2
|
-
|
|
3
|
-
<!-- The problem this solves, not just the diff. If it fixes a bug,
|
|
4
|
-
say how you found and verified it. -->
|
|
5
|
-
|
|
6
|
-
## Scope check
|
|
7
|
-
|
|
8
|
-
- [ ] I've read `README.md`'s "What this is NOT" section and confirmed
|
|
9
|
-
this doesn't add login, billing, multi-tenant key custody, or a
|
|
10
|
-
hosted-service feature.
|
|
11
|
-
|
|
12
|
-
## Testing
|
|
13
|
-
|
|
14
|
-
<!-- What you ran, and what it proved. "npm test passes" is fine for a
|
|
15
|
-
small change; describe manual verification for anything that couldn't
|
|
16
|
-
be covered by the existing suite (e.g. a real provider call). -->
|
|
17
|
-
|
|
18
|
-
- [ ] `npm test` passes locally
|
|
19
|
-
- [ ] Added/updated tests for the behavior this changes, or explained
|
|
20
|
-
why that's not practical (e.g. it needs a live provider call)
|
|
21
|
-
|
|
22
|
-
## Anything reviewers should look at closely
|
|
23
|
-
|
|
24
|
-
<!-- Optional - a tricky edge case, a tradeoff you're not fully sure
|
|
25
|
-
about, a place you deviated from an existing pattern in the codebase. -->
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
name: Tests
|
|
2
|
-
|
|
3
|
-
# Runs on every push and PR - unlike this monorepo's own root
|
|
4
|
-
# .github/workflows/e2e.yml (currently workflow_dispatch-only), this
|
|
5
|
-
# is a public OSS repo once extracted: a green check on every PR IS
|
|
6
|
-
# the trust signal step 8 of OPEN_SOURCE_ROADMAP.md exists for, so
|
|
7
|
-
# automatic triggers are the whole point here, not something to dial
|
|
8
|
-
# back to manual.
|
|
9
|
-
on:
|
|
10
|
-
push:
|
|
11
|
-
branches: [main]
|
|
12
|
-
pull_request:
|
|
13
|
-
|
|
14
|
-
jobs:
|
|
15
|
-
test:
|
|
16
|
-
runs-on: ubuntu-latest
|
|
17
|
-
|
|
18
|
-
# metrics-postgres.test.js needs a REAL reachable Postgres - it
|
|
19
|
-
# fails loudly (ECONNREFUSED) rather than skipping if one isn't
|
|
20
|
-
# running, by design (see that file's own header comment). Values
|
|
21
|
-
# here match its own hardcoded fallback connection string exactly
|
|
22
|
-
# (MEMOCODE_ROUTER_TEST_DATABASE_URL's default), so no extra env
|
|
23
|
-
# wiring is needed below.
|
|
24
|
-
services:
|
|
25
|
-
postgres:
|
|
26
|
-
image: postgres:16
|
|
27
|
-
env:
|
|
28
|
-
POSTGRES_USER: postgres
|
|
29
|
-
POSTGRES_PASSWORD: dryrun
|
|
30
|
-
POSTGRES_DB: router_metrics_dryrun
|
|
31
|
-
ports:
|
|
32
|
-
- 5432:5432
|
|
33
|
-
options: >-
|
|
34
|
-
--health-cmd pg_isready
|
|
35
|
-
--health-interval 10s
|
|
36
|
-
--health-timeout 5s
|
|
37
|
-
--health-retries 5
|
|
38
|
-
|
|
39
|
-
strategy:
|
|
40
|
-
# Multiple Node versions, not just one: package.json declares
|
|
41
|
-
# "engines": {"node": ">=18.0.0"} - running the suite against all
|
|
42
|
-
# three actually backs that claim instead of just asserting it.
|
|
43
|
-
matrix:
|
|
44
|
-
node-version: ['18.x', '20.x', '22.x']
|
|
45
|
-
|
|
46
|
-
steps:
|
|
47
|
-
- uses: actions/checkout@v4
|
|
48
|
-
|
|
49
|
-
- uses: actions/setup-node@v4
|
|
50
|
-
with:
|
|
51
|
-
node-version: ${{ matrix.node-version }}
|
|
52
|
-
|
|
53
|
-
# semanticCache.test.js spawns its own throwaway redis-server
|
|
54
|
-
# process directly (a real instance, not a mock) - ubuntu-latest
|
|
55
|
-
# doesn't ship the binary, so it has to be installed explicitly.
|
|
56
|
-
# This is NOT the same thing as the Postgres service above: that
|
|
57
|
-
# test connects to an already-running external service, this one
|
|
58
|
-
# starts and stops its own process on a random port.
|
|
59
|
-
- name: Install redis-server
|
|
60
|
-
run: sudo apt-get update && sudo apt-get install -y redis-server
|
|
61
|
-
|
|
62
|
-
- run: npm ci
|
|
63
|
-
- run: npm test
|
package/CODE_OF_CONDUCT.md
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
# Contributor Covenant Code of Conduct
|
|
2
|
-
|
|
3
|
-
## Our Pledge
|
|
4
|
-
|
|
5
|
-
We as members, contributors, and leaders pledge to make participation in
|
|
6
|
-
our community a harassment-free experience for everyone, regardless of
|
|
7
|
-
age, body size, visible or invisible disability, ethnicity, sex
|
|
8
|
-
characteristics, gender identity and expression, level of experience,
|
|
9
|
-
education, socio-economic status, nationality, personal appearance,
|
|
10
|
-
race, religion, or sexual identity and orientation.
|
|
11
|
-
|
|
12
|
-
We pledge to act and interact in ways that contribute to an open,
|
|
13
|
-
welcoming, diverse, inclusive, and healthy community.
|
|
14
|
-
|
|
15
|
-
## Our Standards
|
|
16
|
-
|
|
17
|
-
Examples of behavior that contributes to a positive environment:
|
|
18
|
-
|
|
19
|
-
* Demonstrating empathy and kindness toward other people
|
|
20
|
-
* Being respectful of differing opinions, viewpoints, and experiences
|
|
21
|
-
* Giving and gracefully accepting constructive feedback
|
|
22
|
-
* Accepting responsibility and apologizing to those affected by our
|
|
23
|
-
mistakes, and learning from the experience
|
|
24
|
-
* Focusing on what is best not just for us as individuals, but for the
|
|
25
|
-
overall community
|
|
26
|
-
|
|
27
|
-
Examples of unacceptable behavior:
|
|
28
|
-
|
|
29
|
-
* The use of sexualized language or imagery, and sexual attention or
|
|
30
|
-
advances of any kind
|
|
31
|
-
* Trolling, insulting or derogatory comments, and personal or political
|
|
32
|
-
attacks
|
|
33
|
-
* Public or private harassment
|
|
34
|
-
* Publishing others' private information, such as a physical or email
|
|
35
|
-
address, without their explicit permission
|
|
36
|
-
* Other conduct which could reasonably be considered inappropriate in a
|
|
37
|
-
professional setting
|
|
38
|
-
|
|
39
|
-
## Enforcement Responsibilities
|
|
40
|
-
|
|
41
|
-
Project maintainers are responsible for clarifying and enforcing our
|
|
42
|
-
standards of acceptable behavior and will take appropriate and fair
|
|
43
|
-
corrective action in response to any behavior that they deem
|
|
44
|
-
inappropriate, threatening, offensive, or harmful.
|
|
45
|
-
|
|
46
|
-
## Scope
|
|
47
|
-
|
|
48
|
-
This Code of Conduct applies within all community spaces (issues, pull
|
|
49
|
-
requests, discussions), and also applies when an individual is
|
|
50
|
-
officially representing the community in public spaces.
|
|
51
|
-
|
|
52
|
-
## Enforcement
|
|
53
|
-
|
|
54
|
-
Instances of abusive, harassing, or otherwise unacceptable behavior may
|
|
55
|
-
be reported to the maintainers via the contact method listed in
|
|
56
|
-
`SECURITY.md`. All complaints will be reviewed and investigated
|
|
57
|
-
promptly and fairly.
|
|
58
|
-
|
|
59
|
-
## Attribution
|
|
60
|
-
|
|
61
|
-
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
|
62
|
-
version 2.1, available at
|
|
63
|
-
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
|
64
|
-
|
|
65
|
-
[homepage]: https://www.contributor-covenant.org
|
|
66
|
-
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
package/CONTRIBUTING.md
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
# Contributing to cachegate
|
|
2
|
-
|
|
3
|
-
Thanks for considering it. This is a small, deliberately focused
|
|
4
|
-
project — read the scope note below before writing code, it'll save
|
|
5
|
-
you a round-trip.
|
|
6
|
-
|
|
7
|
-
## Scope — read this first
|
|
8
|
-
|
|
9
|
-
`README.md`'s "What this is NOT" section is the actual contract, not
|
|
10
|
-
just a marketing caveat: **no login, no billing, no multi-tenant key
|
|
11
|
-
custody, no hosted service** live in this repository, and a PR adding
|
|
12
|
-
any of those will be closed regardless of how well it's built — that
|
|
13
|
-
functionality belongs to a separate, closed product built on top of
|
|
14
|
-
this engine, not this engine itself. This isn't a licensing
|
|
15
|
-
restriction (MIT permits building any of that — see `LICENSE`); it's
|
|
16
|
-
that this repository specifically isn't going to grow into a hosted
|
|
17
|
-
competitor to its own paid product, so a PR heading that direction
|
|
18
|
-
gets closed here regardless of quality, not merged and then diverged
|
|
19
|
-
from later.
|
|
20
|
-
|
|
21
|
-
Everything else — routing strategies, cache behavior, provider
|
|
22
|
-
support, the dashboard, bug fixes — is fair game.
|
|
23
|
-
|
|
24
|
-
## Running it locally
|
|
25
|
-
|
|
26
|
-
```bash
|
|
27
|
-
git clone <this-repo-url>
|
|
28
|
-
cd cachegate
|
|
29
|
-
npm install
|
|
30
|
-
cp .env.example .env # then fill in a real key for at least one provider
|
|
31
|
-
npm start
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
## Running the tests
|
|
35
|
-
|
|
36
|
-
```bash
|
|
37
|
-
npm test
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
This runs Node's built-in test runner (`node --test`) across every file
|
|
41
|
-
in `test/`. A few things worth knowing before you add to it:
|
|
42
|
-
|
|
43
|
-
- **No test here calls a real provider API.** That needs live keys and
|
|
44
|
-
real spend, which isn't reasonable to require of a contributor or a
|
|
45
|
-
CI run. Provider-facing logic (`providers/anthropic.js`,
|
|
46
|
-
`providers/openai.js`) is tested by factoring the pure,
|
|
47
|
-
non-network parts (see `applyStreamEvent`/`applyStreamChunk`) out
|
|
48
|
-
into directly-testable functions with canned input — follow that
|
|
49
|
-
pattern for new provider logic rather than trying to mock the HTTP
|
|
50
|
-
layer.
|
|
51
|
-
- **Tests need to be genuinely isolated.** Several existing tests reset
|
|
52
|
-
`METRICS_LOG_PATH` to a fresh temp file per test (see
|
|
53
|
-
`router.test.js`'s `freshModules` helper) specifically because
|
|
54
|
-
`metrics.js` reads its config once at require time. If your test
|
|
55
|
-
writes metrics, don't assume a clean slate — either isolate it the
|
|
56
|
-
same way, or place it deliberately last in its file if it needs to
|
|
57
|
-
run after everything else that depends on a clean state (see
|
|
58
|
-
`server.test.js`'s own comment on this for a worked example).
|
|
59
|
-
- All tests must pass before a PR is reviewed. If a test is failing for
|
|
60
|
-
a reason unrelated to your change, say so explicitly in the PR rather
|
|
61
|
-
than silently working around it — that's a real bug worth its own
|
|
62
|
-
issue.
|
|
63
|
-
|
|
64
|
-
## Opening a PR
|
|
65
|
-
|
|
66
|
-
- Explain the *why*, not just the *what* — a one-line "fixes X" is
|
|
67
|
-
fine for a trivial fix, but anything behavioral should say what
|
|
68
|
-
problem it solves and how you verified the fix, the same standard
|
|
69
|
-
this codebase's own commit history holds itself to.
|
|
70
|
-
- If you're touching `router.js`'s scoring logic, `failover.js`'s
|
|
71
|
-
retry logic, or anything else with existing inline documentation
|
|
72
|
-
explaining a past decision (search the file for "why" before
|
|
73
|
-
changing something that looks arbitrary) — it's very likely not
|
|
74
|
-
arbitrary. If you disagree with the reasoning, say so in the PR;
|
|
75
|
-
don't just silently remove it.
|
|
76
|
-
- Small, focused PRs review faster than large ones bundling several
|
|
77
|
-
unrelated changes.
|
|
78
|
-
|
|
79
|
-
**How merges actually work here, stated plainly:** this repository is
|
|
80
|
-
mirrored out from a private internal monorepo where day-to-day
|
|
81
|
-
development happens, rather than the other way around. Your PR gets
|
|
82
|
-
reviewed and, once approved, merged here on GitHub like normal — but
|
|
83
|
-
it's also manually reapplied on the internal side afterward, through
|
|
84
|
-
that project's own review process, rather than auto-syncing. In
|
|
85
|
-
practice this means a short delay between "merged here" and "in the
|
|
86
|
-
next internal release," not a rejection — you'll see it land in a
|
|
87
|
-
tagged release once that happens.
|
|
88
|
-
|
|
89
|
-
## Reporting a bug vs. reporting a security issue
|
|
90
|
-
|
|
91
|
-
Regular bugs: open a GitHub issue. Security vulnerabilities (anything
|
|
92
|
-
that could let a request bypass auth, leak another deployment's data,
|
|
93
|
-
or exhaust resources in a way rate limiting doesn't already cover): see
|
|
94
|
-
`SECURITY.md` instead — please don't file those as public issues.
|
package/Dockerfile
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
FROM node:20-slim
|
|
2
|
-
|
|
3
|
-
# Standard OCI labels - GHCR reads image.source automatically on every push
|
|
4
|
-
# and uses it to link this package to the repo (the alternative to clicking
|
|
5
|
-
# "Connect Repository" by hand once, which doesn't survive a re-publish).
|
|
6
|
-
LABEL org.opencontainers.image.source="https://github.com/iDebunk/cachegate" \
|
|
7
|
-
org.opencontainers.image.description="Self-hostable, OpenAI-compatible LLM proxy: routes to the cheapest healthy provider, caches responses exactly and semantically, tracks cost and latency per call." \
|
|
8
|
-
org.opencontainers.image.licenses="MIT"
|
|
9
|
-
|
|
10
|
-
WORKDIR /app
|
|
11
|
-
COPY package*.json ./
|
|
12
|
-
RUN npm ci --omit=dev
|
|
13
|
-
COPY . .
|
|
14
|
-
# The official Node images already ship a non-root "node" user (uid 1000) -
|
|
15
|
-
# use it instead of running as root, standard practice for a public image.
|
|
16
|
-
RUN chown -R node:node /app
|
|
17
|
-
USER node
|
|
18
|
-
EXPOSE 4000
|
|
19
|
-
# No curl/wget in the slim base image - Node's own http module does the
|
|
20
|
-
# check instead. Uses the real GET /health endpoint (public, no auth,
|
|
21
|
-
# exists specifically for this).
|
|
22
|
-
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
|
23
|
-
CMD node -e "require('http').get('http://localhost:'+(process.env.PORT||4000)+'/health',(r)=>process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))"
|
|
24
|
-
CMD ["node", "server.js"]
|
package/SECURITY.md
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
# Security Policy
|
|
2
|
-
|
|
3
|
-
## Reporting a vulnerability
|
|
4
|
-
|
|
5
|
-
**Please do not open a public GitHub issue for a security
|
|
6
|
-
vulnerability.** A public issue is a disclosure before a fix exists.
|
|
7
|
-
|
|
8
|
-
Instead, use GitHub's private vulnerability reporting:
|
|
9
|
-
|
|
10
|
-
1. Go to this repository's **Security** tab.
|
|
11
|
-
2. Click **Report a vulnerability**.
|
|
12
|
-
3. Describe the issue — what it is, how to reproduce it, and its
|
|
13
|
-
likely impact (e.g. "bypasses auth," "leaks another deployment's
|
|
14
|
-
cached data," "exhausts memory regardless of rate limiting").
|
|
15
|
-
|
|
16
|
-
This opens a private conversation with the maintainers, visible only to
|
|
17
|
-
you and them, and lets a fix be prepared and released before the
|
|
18
|
-
vulnerability is public.
|
|
19
|
-
|
|
20
|
-
*(Maintainer note, remove once live: this requires "Private vulnerability
|
|
21
|
-
reporting" to be turned on for the repository — Settings → Security →
|
|
22
|
-
Private vulnerability reporting — as part of step 15's repo setup.)*
|
|
23
|
-
|
|
24
|
-
## What counts as a security issue here
|
|
25
|
-
|
|
26
|
-
Concretely, for this project: anything that lets a request bypass
|
|
27
|
-
`MODEL_ROUTER_INTERNAL_KEY` auth, read or corrupt another deployment's
|
|
28
|
-
cached data or metrics, or exhaust CPU/memory/Redis storage in a way
|
|
29
|
-
`RATE_LIMIT_MAX`/`RATE_LIMIT_WINDOW_MS` doesn't already bound. A
|
|
30
|
-
provider returning an unexpected error, a routing decision you disagree
|
|
31
|
-
with, or a missing feature are regular bugs — open a normal issue for
|
|
32
|
-
those (see `CONTRIBUTING.md`).
|
|
33
|
-
|
|
34
|
-
## Supported versions
|
|
35
|
-
|
|
36
|
-
This project is pre-1.0 (see `OPEN_SOURCE_ROADMAP.md` on the version
|
|
37
|
-
plan) — security fixes go into the latest release only. Once a stable
|
|
38
|
-
1.0 line exists, this section will name which major versions still
|
|
39
|
-
receive fixes.
|
package/sync-oss-release.sh
DELETED
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
#
|
|
3
|
-
# sync-oss-release.sh - mirrors THIS directory's git-tracked files into
|
|
4
|
-
# a checkout of the public cachegate repo, as ONE NEW COMMIT there.
|
|
5
|
-
#
|
|
6
|
-
# See OPEN_SOURCE_ROADMAP.md step 11 for why this direction (this
|
|
7
|
-
# monorepo directory is the source of truth, not the public repo) and
|
|
8
|
-
# step 1 for why the ONE-TIME initial extraction (step 16) uses fresh,
|
|
9
|
-
# curated history. This script is different from that: it's what runs
|
|
10
|
-
# on every sync AFTER the initial extraction, and it does NOT rewrite
|
|
11
|
-
# history - it adds a single normal commit on top of whatever the
|
|
12
|
-
# target repo already has, exactly like any other change to that repo.
|
|
13
|
-
# Rewriting history on every sync would break clones, forks, and
|
|
14
|
-
# in-flight PRs on the public side; that's not what this does.
|
|
15
|
-
#
|
|
16
|
-
# This is a MIRROR, not a merge: after syncing, the target's tracked
|
|
17
|
-
# files exactly match this directory's. A file that exists only in the
|
|
18
|
-
# target (added directly on GitHub, not here) gets REMOVED on sync.
|
|
19
|
-
# That's deliberate - if a file should persist in the public repo, add
|
|
20
|
-
# it here, in the monorepo, since this directory is the source of truth
|
|
21
|
-
# (see step 11). This script will refuse to run against a target that
|
|
22
|
-
# isn't a git repository, specifically so an accidental wipe of some
|
|
23
|
-
# unrelated directory can't happen by pointing this at the wrong path.
|
|
24
|
-
#
|
|
25
|
-
# Usage:
|
|
26
|
-
# ./sync-oss-release.sh <path-to-public-repo-checkout> [--version X.Y.Z]
|
|
27
|
-
#
|
|
28
|
-
# What it does, in order:
|
|
29
|
-
# 1. Refuses to run if the secrets scan (same patterns as the manual
|
|
30
|
-
# step-3 audit: API key shapes, email addresses) finds anything in
|
|
31
|
-
# this directory's tracked files - loud failure, nothing touched,
|
|
32
|
-
# rather than a quiet publish of a leak.
|
|
33
|
-
# 2. If --version is given, bumps THIS directory's own package.json
|
|
34
|
-
# to that version first, so the synced copy carries it too. Omits
|
|
35
|
-
# this by default - the script doesn't invent a version-bump
|
|
36
|
-
# policy on its own (see step 6's deferred semver plan); a
|
|
37
|
-
# no-flag run is a plain resync at whatever version is already
|
|
38
|
-
# set (e.g. reapplying a cherry-picked external PR - see
|
|
39
|
-
# CONTRIBUTING.md's note on that flow).
|
|
40
|
-
# 3. Mirrors every git-tracked file from this directory into the
|
|
41
|
-
# target checkout (removes everything else from the target's
|
|
42
|
-
# working tree first, except its own .git/) - a file removed here
|
|
43
|
-
# also disappears there, never a manual, error-prone diff to keep
|
|
44
|
-
# in sync by hand.
|
|
45
|
-
# 4. Commits in the TARGET repo (one new commit, normal history).
|
|
46
|
-
# Does NOT push - pushing is a deliberate, separate, human/CI step.
|
|
47
|
-
|
|
48
|
-
set -euo pipefail
|
|
49
|
-
|
|
50
|
-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
51
|
-
cd "$SCRIPT_DIR"
|
|
52
|
-
|
|
53
|
-
TARGET=""
|
|
54
|
-
NEW_VERSION=""
|
|
55
|
-
|
|
56
|
-
while [[ $# -gt 0 ]]; do
|
|
57
|
-
case "$1" in
|
|
58
|
-
--version)
|
|
59
|
-
NEW_VERSION="$2"
|
|
60
|
-
shift 2
|
|
61
|
-
;;
|
|
62
|
-
*)
|
|
63
|
-
if [[ -z "$TARGET" ]]; then
|
|
64
|
-
TARGET="$1"
|
|
65
|
-
shift
|
|
66
|
-
else
|
|
67
|
-
echo "Unexpected extra argument: $1" >&2
|
|
68
|
-
exit 1
|
|
69
|
-
fi
|
|
70
|
-
;;
|
|
71
|
-
esac
|
|
72
|
-
done
|
|
73
|
-
|
|
74
|
-
if [[ -z "$TARGET" ]]; then
|
|
75
|
-
echo "Usage: $0 <path-to-public-repo-checkout> [--version X.Y.Z]" >&2
|
|
76
|
-
exit 1
|
|
77
|
-
fi
|
|
78
|
-
|
|
79
|
-
if [[ ! -d "$TARGET/.git" ]]; then
|
|
80
|
-
echo "❌ Refusing to run: $TARGET is not a git repository (no .git/ found)." >&2
|
|
81
|
-
echo " This is deliberate - pointing this at the wrong path would wipe it." >&2
|
|
82
|
-
exit 1
|
|
83
|
-
fi
|
|
84
|
-
|
|
85
|
-
TARGET="$(cd "$TARGET" && pwd)"
|
|
86
|
-
|
|
87
|
-
echo "🔍 Step 1/4: scanning tracked files for secrets before touching anything..."
|
|
88
|
-
# Same shape of check as step 3's manual audit: API key patterns and
|
|
89
|
-
# email addresses, restricted to git-tracked files only (never
|
|
90
|
-
# node_modules, .env, data/ - those aren't tracked, so git ls-files
|
|
91
|
-
# already excludes them).
|
|
92
|
-
SECRET_HIT=0
|
|
93
|
-
while IFS= read -r -d '' file; do
|
|
94
|
-
if grep -qE "sk-[a-zA-Z0-9_-]{20,}|AIza[0-9A-Za-z_-]{20,}|xai-[a-zA-Z0-9_-]{20,}" "$file" 2>/dev/null; then
|
|
95
|
-
echo " ❌ Possible API key in $file" >&2
|
|
96
|
-
SECRET_HIT=1
|
|
97
|
-
fi
|
|
98
|
-
if grep -qE "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-z]{2,}" "$file" 2>/dev/null; then
|
|
99
|
-
# .env.example intentionally has no real emails; this still flags
|
|
100
|
-
# anything matching the shape so a human confirms it's a placeholder.
|
|
101
|
-
match=$(grep -oE "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-z]{2,}" "$file" | grep -v -E "example\.com|your-|@anthropic\.com" || true)
|
|
102
|
-
if [[ -n "$match" ]]; then
|
|
103
|
-
echo " ⚠️ Email-shaped string in $file: $match (confirm this is a placeholder, not real)" >&2
|
|
104
|
-
SECRET_HIT=1
|
|
105
|
-
fi
|
|
106
|
-
fi
|
|
107
|
-
done < <(git ls-files -z)
|
|
108
|
-
|
|
109
|
-
if [[ "$SECRET_HIT" -eq 1 ]]; then
|
|
110
|
-
echo "❌ Aborting sync - resolve the findings above first. Nothing was copied." >&2
|
|
111
|
-
exit 1
|
|
112
|
-
fi
|
|
113
|
-
echo " ✅ Clean."
|
|
114
|
-
|
|
115
|
-
if [[ -n "$NEW_VERSION" ]]; then
|
|
116
|
-
echo "🔢 Step 2/4: bumping package.json to $NEW_VERSION..."
|
|
117
|
-
node -e "
|
|
118
|
-
const fs = require('fs');
|
|
119
|
-
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
|
|
120
|
-
pkg.version = process.argv[1];
|
|
121
|
-
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');
|
|
122
|
-
" "$NEW_VERSION"
|
|
123
|
-
echo " ✅ package.json now at $NEW_VERSION."
|
|
124
|
-
else
|
|
125
|
-
echo "🔢 Step 2/4: no --version given, leaving package.json's version as-is."
|
|
126
|
-
fi
|
|
127
|
-
|
|
128
|
-
echo "📦 Step 3/4: mirroring tracked files into $TARGET..."
|
|
129
|
-
# Wipe the target's working tree except .git/, then copy this
|
|
130
|
-
# directory's tracked files in - guarantees the target ends up an
|
|
131
|
-
# EXACT mirror, not an accumulation of whatever used to be there.
|
|
132
|
-
find "$TARGET" -mindepth 1 -maxdepth 1 -not -name ".git" -exec rm -rf {} +
|
|
133
|
-
|
|
134
|
-
while IFS= read -r -d '' file; do
|
|
135
|
-
dest="$TARGET/$file"
|
|
136
|
-
mkdir -p "$(dirname "$dest")"
|
|
137
|
-
cp "$file" "$dest"
|
|
138
|
-
done < <(git ls-files -z)
|
|
139
|
-
|
|
140
|
-
echo " ✅ Copied $(git ls-files | wc -l | tr -d ' ') tracked files."
|
|
141
|
-
|
|
142
|
-
echo "💾 Step 4/4: committing in the target repo (not pushing)..."
|
|
143
|
-
SOURCE_SHA="$(git rev-parse --short HEAD)"
|
|
144
|
-
(
|
|
145
|
-
cd "$TARGET"
|
|
146
|
-
git add -A
|
|
147
|
-
if git diff --cached --quiet; then
|
|
148
|
-
echo " ℹ️ Nothing changed - target already matches this directory. No commit made."
|
|
149
|
-
else
|
|
150
|
-
git commit -m "Sync from internal monorepo @ ${SOURCE_SHA}
|
|
151
|
-
|
|
152
|
-
Mirrors 210_apps/001_model_router/ as of that commit. This commit was
|
|
153
|
-
generated by sync-oss-release.sh, not written by hand - see
|
|
154
|
-
OPEN_SOURCE_ROADMAP.md step 11 in the source repo for why this
|
|
155
|
-
direction (monorepo -> public repo, not the reverse)."
|
|
156
|
-
echo " ✅ Committed. Review with 'git show' in $TARGET, then push when ready - this script never pushes."
|
|
157
|
-
fi
|
|
158
|
-
)
|
|
159
|
-
|
|
160
|
-
echo "✅ Sync complete."
|
package/test/auth-config.test.js
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
const { test } = require('node:test');
|
|
2
|
-
const assert = require('node:assert/strict');
|
|
3
|
-
|
|
4
|
-
// server.js reads its env vars once at require time, so each case here
|
|
5
|
-
// resets env + require cache before requiring fresh.
|
|
6
|
-
function freshServer(env) {
|
|
7
|
-
delete process.env.MODEL_ROUTER_INTERNAL_KEY;
|
|
8
|
-
delete process.env.ALLOW_INSECURE_LOCAL_DEV;
|
|
9
|
-
Object.assign(process.env, env);
|
|
10
|
-
delete require.cache[require.resolve('../server')];
|
|
11
|
-
return require('../server');
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
test('isAuthConfigured() is false with neither key nor opt-in set', () => {
|
|
15
|
-
const { isAuthConfigured } = freshServer({});
|
|
16
|
-
assert.equal(isAuthConfigured(), false);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
test('isAuthConfigured() is true once MODEL_ROUTER_INTERNAL_KEY is set', () => {
|
|
20
|
-
const { isAuthConfigured } = freshServer({ MODEL_ROUTER_INTERNAL_KEY: 'some-key' });
|
|
21
|
-
assert.equal(isAuthConfigured(), true);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
test('isAuthConfigured() is true with the explicit insecure opt-in, even with no key', () => {
|
|
25
|
-
const { isAuthConfigured } = freshServer({ ALLOW_INSECURE_LOCAL_DEV: 'true' });
|
|
26
|
-
assert.equal(isAuthConfigured(), true);
|
|
27
|
-
});
|
package/test/cache.test.js
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
const { test } = require('node:test');
|
|
2
|
-
const assert = require('node:assert/strict');
|
|
3
|
-
const cache = require('../cache');
|
|
4
|
-
|
|
5
|
-
test('buildCacheKey is deterministic for identical payloads', () => {
|
|
6
|
-
const payload = { model: 'gpt-4o-mini', messages: [{ role: 'user', content: 'hi' }] };
|
|
7
|
-
assert.equal(cache.buildCacheKey(payload), cache.buildCacheKey({ ...payload }));
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
test('buildCacheKey differs when messages differ', () => {
|
|
11
|
-
const a = { model: 'gpt-4o-mini', messages: [{ role: 'user', content: 'hi' }] };
|
|
12
|
-
const b = { model: 'gpt-4o-mini', messages: [{ role: 'user', content: 'bye' }] };
|
|
13
|
-
assert.notEqual(cache.buildCacheKey(a), cache.buildCacheKey(b));
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
test('buildCacheKey differs when model differs, same messages', () => {
|
|
17
|
-
const messages = [{ role: 'user', content: 'hi' }];
|
|
18
|
-
const a = { model: 'gpt-4o-mini', messages };
|
|
19
|
-
const b = { model: 'claude-haiku-4-5-20251001', messages };
|
|
20
|
-
assert.notEqual(cache.buildCacheKey(a), cache.buildCacheKey(b));
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
test('buildCacheKey treats an unset temperature the same as 0.0 (documented default)', () => {
|
|
24
|
-
const a = { model: 'gpt-4o-mini', messages: [{ role: 'user', content: 'hi' }] };
|
|
25
|
-
const b = { model: 'gpt-4o-mini', messages: [{ role: 'user', content: 'hi' }], temperature: 0.0 };
|
|
26
|
-
assert.equal(cache.buildCacheKey(a), cache.buildCacheKey(b));
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
test('isConnected() is false with no REDIS_URL configured', () => {
|
|
30
|
-
// This test suite never sets REDIS_URL, matching the documented
|
|
31
|
-
// graceful-degradation path (cache disabled, not crashed).
|
|
32
|
-
assert.equal(cache.isConnected(), false);
|
|
33
|
-
});
|
package/test/embeddings.test.js
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
const { test } = require('node:test');
|
|
2
|
-
const assert = require('node:assert/strict');
|
|
3
|
-
|
|
4
|
-
function freshEmbeddings(env) {
|
|
5
|
-
delete process.env.OPENAI_API_KEY;
|
|
6
|
-
Object.assign(process.env, env);
|
|
7
|
-
delete require.cache[require.resolve('../embeddings')];
|
|
8
|
-
return require('../embeddings');
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
test('isEnabled() is false with no OPENAI_API_KEY', () => {
|
|
12
|
-
const embeddings = freshEmbeddings({});
|
|
13
|
-
assert.equal(embeddings.isEnabled(), false);
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
test('isEnabled() is true once OPENAI_API_KEY is set', () => {
|
|
17
|
-
const embeddings = freshEmbeddings({ OPENAI_API_KEY: 'sk-test-fake' });
|
|
18
|
-
assert.equal(embeddings.isEnabled(), true);
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
test('embed() rejects clearly when disabled, without attempting a network call', async () => {
|
|
22
|
-
const embeddings = freshEmbeddings({});
|
|
23
|
-
await assert.rejects(() => embeddings.embed('hello'), /OPENAI_API_KEY not configured/);
|
|
24
|
-
});
|