mtok-bridge 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Roy Ashbrook
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,43 @@
1
+ # mtok-bridge
2
+
3
+ Serve any model as an OpenAI-compatible API, gated by a key. No payment, no market, no account,
4
+ nothing reported anywhere. Runs anywhere node runs.
5
+
6
+ ```
7
+ npx mtok-bridge --upstream https://api.openai.com/v1 --upstream-key sk-... --model gpt-4o-mini
8
+ npx mtok-bridge --upstream http://localhost:11434/v1 --model llama3.2 # a local ollama server
9
+ ```
10
+
11
+ It prints an endpoint and an api key. Hand them to whoever should use it:
12
+
13
+ ```
14
+ curl http://localhost:8790/v1/chat/completions \
15
+ -H "authorization: Bearer <the-printed-key>" \
16
+ -H "content-type: application/json" \
17
+ -d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"hi"}]}'
18
+ ```
19
+
20
+ ## flags
21
+
22
+ - `--upstream <url>` (required): any OpenAI-compatible chat/completions root (a provider, or your
23
+ own model server: ollama, LM Studio, vLLM, etc).
24
+ - `--upstream-key <key>`: the upstream's bearer token, if it needs one.
25
+ - `--model <id>`: a model you serve (repeatable, or a comma list). Omit to pass the upstream's
26
+ default through.
27
+ - `--port <n>`: default 8790.
28
+ - `--api-key <key>`: the key clients send. Omit and one is generated + printed for you.
29
+ - `--keyless`: serve with NO key (anyone who can reach the endpoint can use it). Opt-in.
30
+
31
+ ## want to get paid for it?
32
+
33
+ The bridge is the transport half of [mtok.market](https://mtok.market)'s seller relay. When you
34
+ want to get PAID for a model (on-chain, per call, in USDC on Base) and be discovered on the market
35
+ board instead of handing out keys, the market relay wraps this exact bridge with settlement. Same
36
+ tool, one layer on top.
37
+
38
+
39
+ ---
40
+
41
+ Read-only public mirror. The source of truth is the private mtok.market
42
+ monorepo; this repo is synced automatically. Do not open pull requests here.
43
+ Home: https://mtok.market
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env node
2
+ // mtok-bridge CLI: serve a model as an OpenAI-compatible API, keyed, no payment, no market.
3
+ // Runs anywhere node runs. Point it at any OpenAI-compatible upstream (a provider, or your own
4
+ // local model server), hand out the endpoint + key. When you want to get PAID + discovered for
5
+ // the same model, the market relay (npx mtok-relay) wraps this exact core with on-chain settlement.
6
+ //
7
+ // npx mtok-bridge --upstream https://api.openai.com/v1 --upstream-key sk-... --model gpt-4o-mini
8
+ // npx mtok-bridge --upstream http://localhost:11434/v1 --model llama3.2 # a local ollama server
9
+ //
10
+ // Flags: --upstream <url> (required), --upstream-key <key>, --model <id> (repeatable or comma-list),
11
+ // --port <n> (default 8790), --api-key <key> (default: generate + print one), --keyless.
12
+ import http from 'node:http';
13
+ import crypto from 'node:crypto';
14
+ import { serveChat, httpUpstream } from './src/bridge.mjs';
15
+
16
+ function parseArgs(argv) {
17
+ const o = { models: [], port: 8790 };
18
+ for (let i = 0; i < argv.length; i++) {
19
+ const a = argv[i];
20
+ if (a === '--keyless') { o.keyless = true; continue; }
21
+ if (!a.startsWith('--')) { console.error(`ABORT: unexpected argument "${a}"`); process.exit(1); }
22
+ const name = a.slice(2);
23
+ const val = argv[++i];
24
+ if (name === 'model') o.models.push(...String(val || '').split(',').map((s) => s.trim()).filter(Boolean));
25
+ else if (name === 'upstream') o.upstream = val;
26
+ else if (name === 'upstream-key') o.upstreamKey = val;
27
+ else if (name === 'port') o.port = Number(val) || 8790;
28
+ else if (name === 'api-key') o.apiKey = val;
29
+ else { console.error(`ABORT: unknown flag --${name}`); process.exit(1); }
30
+ }
31
+ return o;
32
+ }
33
+
34
+ const o = parseArgs(process.argv.slice(2));
35
+ if (!o.upstream) { console.error('need --upstream <openai-compatible url> (e.g. https://api.openai.com/v1 or http://localhost:11434/v1)'); process.exit(1); }
36
+ const apiKey = o.keyless ? null : (o.apiKey || 'mtok_' + crypto.randomBytes(24).toString('hex'));
37
+ const upstream = httpUpstream({ baseUrl: o.upstream, key: o.upstreamKey });
38
+
39
+ const readBody = (req) => new Promise((resolve) => {
40
+ let data = ''; let over = false;
41
+ req.on('data', (c) => { data += c; if (data.length > 2_000_000) { over = true; req.destroy(); } });
42
+ req.on('end', () => resolve(over ? null : data));
43
+ req.on('error', () => resolve(null));
44
+ });
45
+ const send = (res, status, json) => { res.writeHead(status, { 'content-type': 'application/json' }); res.end(JSON.stringify(json)); };
46
+
47
+ const server = http.createServer(async (req, res) => {
48
+ const path = (req.url || '').split('?')[0];
49
+ if (req.method === 'GET' && path === '/v1/models') {
50
+ return send(res, 200, { object: 'list', data: (o.models.length ? o.models : ['(upstream default)']).map((id) => ({ id, object: 'model' })) });
51
+ }
52
+ if (req.method === 'POST' && path === '/v1/chat/completions') {
53
+ const raw = await readBody(req);
54
+ if (raw === null) return send(res, 413, { error: { message: 'body too large or unreadable', type: 'invalid_request_error' } });
55
+ let body; try { body = JSON.parse(raw || '{}'); } catch { return send(res, 400, { error: { message: 'invalid JSON', type: 'invalid_request_error' } }); }
56
+ const r = await serveChat({ body, authHeader: req.headers['authorization'], apiKey, models: o.models, upstream });
57
+ return send(res, r.status, r.json);
58
+ }
59
+ return send(res, 404, { error: { message: 'not found; POST /v1/chat/completions', type: 'invalid_request_error' } });
60
+ });
61
+
62
+ server.listen(o.port, () => {
63
+ const base = `http://localhost:${o.port}/v1`;
64
+ console.log('mtok-bridge is serving.');
65
+ console.log(` endpoint: ${base}`);
66
+ console.log(` models: ${o.models.length ? o.models.join(', ') : '(whatever the upstream serves)'}`);
67
+ console.log(apiKey ? ` api key: ${apiKey}` : ' api key: NONE (--keyless: anyone who can reach this endpoint can use it)');
68
+ console.log('\n hand these to whoever should use it. example:');
69
+ console.log(` curl ${base}/chat/completions \\`);
70
+ if (apiKey) console.log(` -H "authorization: Bearer ${apiKey}" \\`);
71
+ console.log(` -H "content-type: application/json" \\`);
72
+ console.log(` -d '{"model":"${o.models[0] ?? 'MODEL'}","messages":[{"role":"user","content":"hi"}]}'`);
73
+ console.log('\n want to get PAID for this model instead of handing out keys? the market relay');
74
+ console.log(' (npx mtok-relay) wraps this same bridge with on-chain settlement. see mtok.market.');
75
+ });
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "mtok-bridge",
3
+ "version": "0.1.0",
4
+ "description": "Serve any model as an OpenAI-compatible API with a key. No payment, no market, runs anywhere node runs. The transport core behind mtok.market's seller relay.",
5
+ "type": "module",
6
+ "bin": {
7
+ "mtok-bridge": "mtok-bridge.mjs"
8
+ },
9
+ "exports": {
10
+ ".": "./src/bridge.mjs"
11
+ },
12
+ "files": [
13
+ "mtok-bridge.mjs",
14
+ "src",
15
+ "README.md"
16
+ ],
17
+ "scripts": {
18
+ "test": "node --test"
19
+ },
20
+ "engines": {
21
+ "node": ">=20"
22
+ },
23
+ "keywords": [
24
+ "llm",
25
+ "openai-compatible",
26
+ "proxy",
27
+ "bridge",
28
+ "mtok"
29
+ ],
30
+ "license": "MIT",
31
+ "homepage": "https://mtok.market",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/mtok-market/bridge.git"
35
+ }
36
+ }
package/src/bridge.mjs ADDED
@@ -0,0 +1,71 @@
1
+ // mtok-bridge core: the TRANSPORT guts of a model seller, with NO payment and NO market.
2
+ // Serve any model as an OpenAI-compatible API, gated by an api key. Runtime-agnostic and
3
+ // dependency-free: the request logic is pure functions; a host (the node CLI here, a CF Worker
4
+ // later, the market relay on top) adapts its req/res to them. The market layer (on-chain verify,
5
+ // fee, redemption, discovery) is a SEPARATE wrapper that calls the same serveChat guts; it is not
6
+ // in here. (#566)
7
+
8
+ // Bearer-key auth. No key configured (null/'') = OPEN on purpose (keyless mode is an explicit
9
+ // opt-in the CLI announces loudly). A configured key must match exactly.
10
+ export function checkAuth(authHeader, apiKey) {
11
+ if (!apiKey) return true;
12
+ const m = /^Bearer\s+(.+)$/i.exec(String(authHeader || ''));
13
+ return !!m && m[1] === apiKey;
14
+ }
15
+
16
+ // Resolve the requested model against the served set. Empty set = pass the request through
17
+ // (the upstream decides). One-or-more served models: default to the first when none is asked,
18
+ // and reject a model we do not serve (null = not allowed).
19
+ export function pickModel(requested, models) {
20
+ const set = Array.isArray(models) ? models.filter(Boolean) : [];
21
+ if (!set.length) return requested || null;
22
+ if (!requested) return set[0];
23
+ return set.includes(requested) ? requested : null;
24
+ }
25
+
26
+ // Core chat handler. `upstream(payload)` returns an OpenAI-shaped completion (or throws). Returns
27
+ // { status, json } for the host to serialize. This is the whole free bridge; the market layer
28
+ // wraps it with a verify step in front.
29
+ export async function serveChat({ body, authHeader, apiKey, models, upstream }) {
30
+ if (!checkAuth(authHeader, apiKey)) {
31
+ return { status: 401, json: { error: { message: 'missing or invalid api key', type: 'auth_error' } } };
32
+ }
33
+ if (!body || !Array.isArray(body.messages) || !body.messages.length) {
34
+ return { status: 400, json: { error: { message: 'messages[] is required', type: 'invalid_request_error' } } };
35
+ }
36
+ const model = pickModel(body.model, models);
37
+ if (!model) {
38
+ const served = (Array.isArray(models) ? models : []).join(', ') || '(any)';
39
+ return { status: 400, json: { error: { message: `model "${body.model}" is not served here (served: ${served})`, type: 'invalid_request_error' } } };
40
+ }
41
+ let completion;
42
+ try {
43
+ completion = await upstream({ ...body, model });
44
+ } catch (e) {
45
+ return { status: 502, json: { error: { message: `upstream error: ${e?.message ?? e}`, type: 'upstream_error' } } };
46
+ }
47
+ return { status: 200, json: completion };
48
+ }
49
+
50
+ // Build an upstream function that forwards to any OpenAI-compatible chat/completions endpoint.
51
+ // `baseUrl` is the API root (e.g. https://api.openai.com/v1 or a local model server); `key` is
52
+ // its bearer token (optional for a keyless local server). This is what makes the bridge portable:
53
+ // point it at a provider, or at ollama / LM Studio / vLLM on localhost.
54
+ export function httpUpstream({ baseUrl, key }) {
55
+ const url = String(baseUrl || '').replace(/\/$/, '') + '/chat/completions';
56
+ return async (payload) => {
57
+ const res = await fetch(url, {
58
+ method: 'POST',
59
+ headers: {
60
+ 'content-type': 'application/json',
61
+ ...(key ? { authorization: `Bearer ${key}` } : {}),
62
+ },
63
+ body: JSON.stringify(payload),
64
+ });
65
+ const text = await res.text();
66
+ let json;
67
+ try { json = JSON.parse(text); } catch { throw new Error(`non-JSON upstream response (${res.status})`); }
68
+ if (!res.ok) throw new Error(json?.error?.message || `upstream ${res.status}`);
69
+ return json;
70
+ };
71
+ }