orrery-node 0.0.0-stage → 0.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/LICENSE +21 -0
- package/README.md +137 -2
- package/dist/brand.js +11 -0
- package/dist/chain/program.js +146 -0
- package/dist/chain/voucher.js +38 -0
- package/dist/cli.js +190 -0
- package/dist/keys.js +35 -0
- package/dist/manifest.js +34 -0
- package/dist/pricing.js +22 -0
- package/dist/provider/node.js +352 -0
- package/dist/router/channels.js +130 -0
- package/dist/router/console.js +49 -0
- package/dist/router/policy.js +88 -0
- package/dist/router/registry.js +125 -0
- package/dist/router/server.js +256 -0
- package/dist/seal.js +49 -0
- package/dist/sse.js +41 -0
- package/dist/usage.js +7 -0
- package/package.json +65 -4
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// What the router knows about the network: every Provider account on chain, plus the price manifest each node
|
|
2
|
+
// serves, kept only if its sha256 matches the hash the provider's wallet put on chain.
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import bs58 from 'bs58';
|
|
5
|
+
import { ACCOUNT, decodeProvider, programId } from "../chain/program.js";
|
|
6
|
+
import { checkManifest, manifestHash } from "../manifest.js";
|
|
7
|
+
import { fetchWithTimeout } from "../sse.js";
|
|
8
|
+
export class Registry {
|
|
9
|
+
conn;
|
|
10
|
+
tee;
|
|
11
|
+
entries = new Map();
|
|
12
|
+
refreshedAt = 0;
|
|
13
|
+
constructor(conn, tee) {
|
|
14
|
+
this.conn = conn;
|
|
15
|
+
this.tee = tee;
|
|
16
|
+
}
|
|
17
|
+
async refresh() {
|
|
18
|
+
const accounts = await this.conn.getProgramAccounts(programId(), { filters: [{ memcmp: { offset: 0, bytes: bs58.encode(ACCOUNT.Provider) } }] });
|
|
19
|
+
const seen = new Set();
|
|
20
|
+
await Promise.all(accounts.map(async ({ pubkey, account }) => {
|
|
21
|
+
const address = pubkey.toBase58();
|
|
22
|
+
seen.add(address);
|
|
23
|
+
let p;
|
|
24
|
+
try {
|
|
25
|
+
p = decodeProvider(account.data);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const prev = this.entries.get(address);
|
|
31
|
+
const same = prev && prev.account.manifestHash.equals(p.manifestHash) && prev.account.endpoint === p.endpoint && prev.manifest && Date.now() - prev.checkedAt < 10 * 60_000;
|
|
32
|
+
const entry = same ? { ...prev, account: p } : { address, account: p, manifest: null, problem: null, checkedAt: Date.now(), tee: null };
|
|
33
|
+
if (!same && p.active) {
|
|
34
|
+
try {
|
|
35
|
+
const r = await fetchWithTimeout(`${p.endpoint.replace(/\/$/, '')}/v1/manifest`, { timeoutMs: 4_000 });
|
|
36
|
+
const bytes = Buffer.from(await r.arrayBuffer());
|
|
37
|
+
if (!manifestHash(bytes).equals(p.manifestHash))
|
|
38
|
+
throw new Error('manifest does not match the hash on chain');
|
|
39
|
+
const m = checkManifest(JSON.parse(bytes.toString('utf8')));
|
|
40
|
+
if (m.authority !== p.authority.toBase58())
|
|
41
|
+
throw new Error('manifest names another wallet');
|
|
42
|
+
entry.manifest = m;
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
entry.problem = err.message;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
entry.tee = await this.checkTee(entry);
|
|
49
|
+
this.entries.set(address, entry);
|
|
50
|
+
}));
|
|
51
|
+
for (const k of [...this.entries.keys()])
|
|
52
|
+
if (!seen.has(k))
|
|
53
|
+
this.entries.delete(k);
|
|
54
|
+
this.refreshedAt = Date.now();
|
|
55
|
+
}
|
|
56
|
+
/** A provider's enclave counts only if the report it serves hashes to the on-chain claim, names the on-chain key,
|
|
57
|
+
* is not expired, booted an allowed image, and the hardware verifier accepts it. Dev reports are software
|
|
58
|
+
* stand-ins and pass only when the router is told to accept them for local testing. */
|
|
59
|
+
async checkTee(e) {
|
|
60
|
+
const p = e.account;
|
|
61
|
+
if (p.teeKind === 0 || !p.active)
|
|
62
|
+
return null;
|
|
63
|
+
const enclaveKey = Buffer.from(p.teeKey);
|
|
64
|
+
if (BigInt(Math.floor(Date.now() / 1000)) >= p.teeExpiresAt)
|
|
65
|
+
return { verified: false, dev: false, reason: 'attestation expired', enclaveKey };
|
|
66
|
+
try {
|
|
67
|
+
const r = await fetchWithTimeout(`${p.endpoint.replace(/\/$/, '')}/v1/attestation`, { timeoutMs: 4_000 });
|
|
68
|
+
const a = (await r.json());
|
|
69
|
+
const report = Buffer.from(a.report, 'base64');
|
|
70
|
+
if (!createHash('sha256').update(report).digest().equals(p.teeReportHash))
|
|
71
|
+
return { verified: false, dev: false, reason: 'report does not match the on-chain hash', enclaveKey };
|
|
72
|
+
if (a.enclaveKey !== p.teeKey.toString('hex'))
|
|
73
|
+
return { verified: false, dev: false, reason: 'report names another key', enclaveKey };
|
|
74
|
+
if (this.tee.measurements.length && !this.tee.measurements.includes(p.teeMeasurement.toString('hex')))
|
|
75
|
+
return { verified: false, dev: false, reason: 'image not on your allow list', enclaveKey };
|
|
76
|
+
let dev = false;
|
|
77
|
+
try {
|
|
78
|
+
dev = JSON.parse(report.toString('utf8')).dev === true;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
dev = false;
|
|
82
|
+
}
|
|
83
|
+
if (dev)
|
|
84
|
+
return { verified: this.tee.allowDev, dev: true, reason: this.tee.allowDev ? 'dev report accepted (local testing)' : 'dev report, not hardware', enclaveKey };
|
|
85
|
+
if (!this.tee.verifier)
|
|
86
|
+
return { verified: false, dev: false, reason: 'no hardware verifier configured', enclaveKey };
|
|
87
|
+
const ok = await this.tee.verifier(a, p);
|
|
88
|
+
return { verified: ok, dev: false, reason: ok ? 'hardware report verified' : 'hardware verifier rejected the report', enclaveKey };
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
return { verified: false, dev: false, reason: `attestation fetch failed: ${err.message}`, enclaveKey };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
usable() {
|
|
95
|
+
return [...this.entries.values()].filter((e) => e.account.active && e.manifest && !e.problem);
|
|
96
|
+
}
|
|
97
|
+
offers(model) {
|
|
98
|
+
const out = [];
|
|
99
|
+
for (const e of this.usable()) {
|
|
100
|
+
const offer = e.manifest.models.find((m) => m.id === model);
|
|
101
|
+
if (offer)
|
|
102
|
+
out.push({ entry: e, offer });
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
catalog() {
|
|
107
|
+
const by = new Map();
|
|
108
|
+
for (const e of this.usable()) {
|
|
109
|
+
for (const m of e.manifest.models) {
|
|
110
|
+
const c = by.get(m.id) ?? { id: m.id, providers: 0, cheapest: m.price, context: m.context, private: 0 };
|
|
111
|
+
c.providers += 1;
|
|
112
|
+
if (m.price.input + m.price.output < c.cheapest.input + c.cheapest.output)
|
|
113
|
+
c.cheapest = m.price;
|
|
114
|
+
c.context = Math.max(c.context, m.context);
|
|
115
|
+
if (e.tee?.verified)
|
|
116
|
+
c.private += 1;
|
|
117
|
+
by.set(m.id, c);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return [...by.values()].sort((a, b) => b.providers - a.providers || a.id.localeCompare(b.id));
|
|
121
|
+
}
|
|
122
|
+
get(address) {
|
|
123
|
+
return this.entries.get(typeof address === 'string' ? address : address.toBase58());
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
// The router: an OpenAI-compatible endpoint on 127.0.0.1. Point any tool at http://127.0.0.1:7171/v1 and it picks a
|
|
2
|
+
// provider per request, pays it through a channel, and falls back to the next provider if one fails.
|
|
3
|
+
import { createServer } from 'node:http';
|
|
4
|
+
import { costOf, estimateInputTokens, usd } from "../pricing.js";
|
|
5
|
+
import { decrypt, sealRequest } from "../seal.js";
|
|
6
|
+
import { fetchWithTimeout, readSse, sseFrame } from "../sse.js";
|
|
7
|
+
import { parseModel, rank, Stats } from "./policy.js";
|
|
8
|
+
import { consolePage } from "./console.js";
|
|
9
|
+
class Unserved extends Error {
|
|
10
|
+
status;
|
|
11
|
+
code;
|
|
12
|
+
constructor(message, status = 502, code = 'no_provider') {
|
|
13
|
+
super(message);
|
|
14
|
+
this.status = status;
|
|
15
|
+
this.code = code;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async function readJson(req) {
|
|
19
|
+
const parts = [];
|
|
20
|
+
let n = 0;
|
|
21
|
+
for await (const c of req) {
|
|
22
|
+
n += c.length;
|
|
23
|
+
if (n > 8 * 1024 * 1024)
|
|
24
|
+
throw new Unserved('request too large', 413, 'too_large');
|
|
25
|
+
parts.push(c);
|
|
26
|
+
}
|
|
27
|
+
return JSON.parse(Buffer.concat(parts).toString('utf8') || '{}');
|
|
28
|
+
}
|
|
29
|
+
const send = (res, status, body, headers = {}) => {
|
|
30
|
+
res.writeHead(status, { 'content-type': 'application/json', ...headers });
|
|
31
|
+
res.end(JSON.stringify(body, (_, v) => (typeof v === 'bigint' ? v.toString() : v)));
|
|
32
|
+
};
|
|
33
|
+
export class Router {
|
|
34
|
+
o;
|
|
35
|
+
stats = new Stats();
|
|
36
|
+
trips = [];
|
|
37
|
+
server = null;
|
|
38
|
+
timer = null;
|
|
39
|
+
inflight = new Map();
|
|
40
|
+
log;
|
|
41
|
+
constructor(o) {
|
|
42
|
+
this.o = o;
|
|
43
|
+
this.log = o.log ?? ((l) => console.log(l));
|
|
44
|
+
}
|
|
45
|
+
async start() {
|
|
46
|
+
await this.o.registry.refresh().catch((e) => this.log(`registry refresh failed: ${e.message}`));
|
|
47
|
+
this.timer = setInterval(() => void this.o.registry.refresh().catch(() => undefined), (this.o.refreshSecs ?? 60) * 1000);
|
|
48
|
+
this.server = createServer((req, res) => void this.handle(req, res));
|
|
49
|
+
await new Promise((r) => this.server.listen(this.o.port, this.o.host ?? '127.0.0.1', () => r()));
|
|
50
|
+
return this.server;
|
|
51
|
+
}
|
|
52
|
+
close() {
|
|
53
|
+
if (this.timer)
|
|
54
|
+
clearInterval(this.timer);
|
|
55
|
+
return new Promise((r) => (this.server ? this.server.close(() => r()) : r()));
|
|
56
|
+
}
|
|
57
|
+
async handle(req, res) {
|
|
58
|
+
const url = new URL(req.url ?? '/', 'http://x');
|
|
59
|
+
const origin = req.headers.origin;
|
|
60
|
+
if (origin && !/^https?:\/\/(127\.0\.0\.1|localhost)(:\d+)?$/.test(origin))
|
|
61
|
+
return send(res, 403, { error: { code: 'origin', message: 'the router only answers this computer' } });
|
|
62
|
+
try {
|
|
63
|
+
if (req.method === 'GET' && (url.pathname === '/v1/models' || url.pathname === '/models'))
|
|
64
|
+
return this.models(res);
|
|
65
|
+
if (req.method === 'POST' && (url.pathname === '/v1/chat/completions' || url.pathname === '/chat/completions'))
|
|
66
|
+
return await this.chat(req, res);
|
|
67
|
+
if (req.method === 'GET' && url.pathname === '/orrery/status')
|
|
68
|
+
return this.status(res);
|
|
69
|
+
if (req.method === 'GET' && url.pathname === '/') {
|
|
70
|
+
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
|
|
71
|
+
return void res.end(consolePage());
|
|
72
|
+
}
|
|
73
|
+
return send(res, 404, { error: { code: 'not_found', message: 'no such route' } });
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
const e = err;
|
|
77
|
+
if (!res.headersSent)
|
|
78
|
+
return send(res, e.status ?? 500, { error: { code: e.code ?? 'internal', message: e.message } });
|
|
79
|
+
res.end();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
models(res) {
|
|
83
|
+
const data = this.o.registry.catalog().map((m) => ({
|
|
84
|
+
id: m.id, object: 'model', owned_by: 'network', providers: m.providers, private_providers: m.private, context_length: m.context,
|
|
85
|
+
pricing: { prompt_per_million: m.cheapest.input / 1e6, completion_per_million: m.cheapest.output / 1e6 },
|
|
86
|
+
}));
|
|
87
|
+
send(res, 200, { object: 'list', data });
|
|
88
|
+
}
|
|
89
|
+
status(res) {
|
|
90
|
+
const providers = [...this.o.registry.entries.values()].map((e) => ({
|
|
91
|
+
address: e.address, name: e.manifest?.name ?? null, endpoint: e.account.endpoint, active: e.account.active, problem: e.problem,
|
|
92
|
+
models: e.manifest?.models.length ?? 0, settled: usd(e.account.settled, 2), redeems: Number(e.account.redeems),
|
|
93
|
+
tee: e.tee ? { verified: e.tee.verified, dev: e.tee.dev, reason: e.tee.reason } : null, observed: this.stats.get(e.address),
|
|
94
|
+
}));
|
|
95
|
+
const channels = this.o.channels.list().map((c) => ({ provider: c.provider, channel: c.channel, deposit: usd(c.deposit), spent: usd(c.cumulative), left: usd(c.deposit - c.cumulative) }));
|
|
96
|
+
send(res, 200, { route: this.o.route, refreshedAt: this.o.registry.refreshedAt, spentToday: usd(this.o.channels.todaySpent()), providers, channels, trips: this.trips.slice(-50).reverse() });
|
|
97
|
+
}
|
|
98
|
+
async chat(req, res) {
|
|
99
|
+
const body = await readJson(req);
|
|
100
|
+
if (typeof body.model !== 'string')
|
|
101
|
+
throw new Unserved('model is required', 400, 'bad_request');
|
|
102
|
+
const header = String(req.headers['x-orrery-route'] ?? '');
|
|
103
|
+
const { model, route } = parseModel(body.model, ['auto', 'price', 'speed', 'reputation', 'privacy'].includes(header) ? header : this.o.route);
|
|
104
|
+
const pin = req.headers['x-orrery-provider'] ? String(req.headers['x-orrery-provider']) : null;
|
|
105
|
+
const wantsStream = body.stream === true;
|
|
106
|
+
const estIn = estimateInputTokens(body);
|
|
107
|
+
const estOut = Number(body.max_completion_tokens ?? body.max_tokens ?? 1_024);
|
|
108
|
+
let offers = this.o.registry.offers(model);
|
|
109
|
+
if (pin)
|
|
110
|
+
offers = offers.filter((o) => o.entry.address.startsWith(pin));
|
|
111
|
+
if (!offers.length)
|
|
112
|
+
throw new Unserved(`no provider on the network serves ${model}${pin ? ` as ${pin}` : ''}`, 404, 'model_not_found');
|
|
113
|
+
const ranked = rank(offers, route, estIn, estOut, this.stats, this.o.minReputation);
|
|
114
|
+
if (!ranked.length)
|
|
115
|
+
throw new Unserved(route === 'privacy' ? `no provider with a verified enclave serves ${model}` : `every provider for ${model} is cooling down`, 503, 'no_provider');
|
|
116
|
+
if (this.o.channels.overBudget(ranked[0].expected))
|
|
117
|
+
throw new Unserved('daily spending cap reached (orrery router --daily-cap)', 429, 'budget');
|
|
118
|
+
const tried = [];
|
|
119
|
+
for (const cand of ranked.slice(0, 3)) {
|
|
120
|
+
try {
|
|
121
|
+
await this.forward(cand, body, wantsStream, res, route, tried.length);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
tried.push(`${cand.entry.manifest?.name ?? cand.entry.address.slice(0, 6)}: ${err.message}`);
|
|
126
|
+
this.stats.fail(cand.entry.address);
|
|
127
|
+
if (res.headersSent) {
|
|
128
|
+
res.end();
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
throw new Unserved(`all providers failed: ${tried.join('; ')}`, 502, 'all_failed');
|
|
134
|
+
}
|
|
135
|
+
async forward(cand, body, wantsStream, res, route, fellThrough, retried = false) {
|
|
136
|
+
const address = cand.entry.address;
|
|
137
|
+
const maxOut = Math.min(Number(body.max_completion_tokens ?? body.max_tokens ?? cand.offer.maxOutput), cand.offer.maxOutput);
|
|
138
|
+
const maxCost = costOf(cand.offer.price, estimateInputTokens(body), maxOut);
|
|
139
|
+
const ch = await this.o.channels.ensure(address, maxCost);
|
|
140
|
+
const payload = JSON.stringify({ ...body, model: cand.offer.id, stream: true });
|
|
141
|
+
let wire = payload;
|
|
142
|
+
let key = null;
|
|
143
|
+
if (cand.sealed) {
|
|
144
|
+
if (!cand.entry.tee?.verified)
|
|
145
|
+
throw new Error('enclave not verified');
|
|
146
|
+
const s = sealRequest(cand.entry.tee.enclaveKey, Buffer.from(payload));
|
|
147
|
+
wire = s.body;
|
|
148
|
+
key = s.key;
|
|
149
|
+
}
|
|
150
|
+
const headers = { 'content-type': 'application/json', 'x-orrery-channel': ch.channel };
|
|
151
|
+
if (ch.voucher)
|
|
152
|
+
headers['x-orrery-voucher'] = ch.voucher;
|
|
153
|
+
const t0 = Date.now();
|
|
154
|
+
this.inflight.set(address, maxCost);
|
|
155
|
+
const r = await fetchWithTimeout(`${cand.entry.account.endpoint.replace(/\/$/, '')}/v1/chat/completions`, { method: 'POST', headers, body: wire, timeoutMs: 20_000 });
|
|
156
|
+
if (r.status === 402 && !retried) {
|
|
157
|
+
const j = (await r.json());
|
|
158
|
+
if (j.error.code === 'payment_required' && j.error.unpaid?.length === 1) {
|
|
159
|
+
const u = j.error.unpaid[0];
|
|
160
|
+
const allowed = this.inflight.get(address) ?? 0n;
|
|
161
|
+
if (BigInt(u.cost) <= allowed && u.model === cand.offer.id) {
|
|
162
|
+
this.o.channels.settle(address, { ...u, cost: BigInt(u.cost) });
|
|
163
|
+
return this.forward(cand, body, wantsStream, res, route, fellThrough, true);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (j.error.code === 'channel_exhausted') {
|
|
167
|
+
await this.o.channels.topUp(address, BigInt(j.error.need ?? '0'));
|
|
168
|
+
return this.forward(cand, body, wantsStream, res, route, fellThrough, true);
|
|
169
|
+
}
|
|
170
|
+
throw new Error(`payment refused: ${j.error.code}`);
|
|
171
|
+
}
|
|
172
|
+
if (!r.ok || !r.body)
|
|
173
|
+
throw new Error(`provider answered ${r.status}`);
|
|
174
|
+
let ttft = 0;
|
|
175
|
+
let record = null;
|
|
176
|
+
const chunks = [];
|
|
177
|
+
let started = false;
|
|
178
|
+
for await (const ev of readSse(r.body)) {
|
|
179
|
+
let data = ev.data;
|
|
180
|
+
if (data === '[DONE]')
|
|
181
|
+
break;
|
|
182
|
+
if (key)
|
|
183
|
+
data = decrypt(key, Buffer.from(data, 'base64')).toString('utf8');
|
|
184
|
+
if (ev.event === 'orrery') {
|
|
185
|
+
const y = JSON.parse(data);
|
|
186
|
+
record = { requestId: y.requestId, model: y.model, inputTokens: y.inputTokens, outputTokens: y.outputTokens, cost: BigInt(y.cost), owed: y.owed };
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (!ttft)
|
|
190
|
+
ttft = Date.now() - t0;
|
|
191
|
+
if (wantsStream) {
|
|
192
|
+
if (!started) {
|
|
193
|
+
res.writeHead(200, { 'content-type': 'text/event-stream', 'cache-control': 'no-cache', 'x-orrery-provider': address, 'x-orrery-route': route, ...(key ? { 'x-orrery-sealed': '1' } : {}) });
|
|
194
|
+
started = true;
|
|
195
|
+
}
|
|
196
|
+
res.write(sseFrame(data));
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
chunks.push(JSON.parse(data));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (!record)
|
|
203
|
+
throw new Error('provider closed the stream without a usage record');
|
|
204
|
+
const ours = costOf(cand.offer.price, record.inputTokens, record.outputTokens);
|
|
205
|
+
const fair = record.cost <= ours && record.cost <= maxCost ? record.cost : ours < maxCost ? ours : maxCost;
|
|
206
|
+
if (fair !== record.cost)
|
|
207
|
+
this.log(`provider ${address.slice(0, 6)} billed ${record.cost} but its own prices say ${ours}; paying ${fair}`);
|
|
208
|
+
const voucher = this.o.channels.settle(address, { ...record, cost: fair });
|
|
209
|
+
this.inflight.delete(address);
|
|
210
|
+
void fetchWithTimeout(`${cand.entry.account.endpoint.replace(/\/$/, '')}/orrery/voucher`, {
|
|
211
|
+
method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ channel: ch.channel, voucher }), timeoutMs: 5_000,
|
|
212
|
+
}).catch(() => undefined);
|
|
213
|
+
this.stats.ok(address, ttft);
|
|
214
|
+
const trip = {
|
|
215
|
+
at: Date.now(), model: cand.offer.id, route, provider: address, providerName: cand.entry.manifest?.name ?? address.slice(0, 6),
|
|
216
|
+
inputTokens: record.inputTokens, outputTokens: record.outputTokens, cost: usd(fair), ttftMs: ttft, totalMs: Date.now() - t0, sealed: !!key, fellThrough,
|
|
217
|
+
};
|
|
218
|
+
this.trips.push(trip);
|
|
219
|
+
if (this.trips.length > 500)
|
|
220
|
+
this.trips.splice(0, this.trips.length - 500);
|
|
221
|
+
this.log(`${route.padEnd(10)} ${cand.offer.id} → ${trip.providerName} ${record.inputTokens}+${record.outputTokens} tok $${trip.cost} ttft ${ttft}ms${key ? ' sealed' : ''}`);
|
|
222
|
+
const orreryHeaders = { 'x-orrery-provider': address, 'x-orrery-route': route, 'x-orrery-cost': trip.cost };
|
|
223
|
+
if (wantsStream) {
|
|
224
|
+
res.write(sseFrame('[DONE]'));
|
|
225
|
+
res.end();
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
send(res, 200, assemble(chunks, cand.offer.id, record.inputTokens, record.outputTokens), orreryHeaders);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
/** Turn a provider's stream back into the single object a non-streaming OpenAI client expects. */
|
|
232
|
+
function assemble(chunks, model, prompt, completion) {
|
|
233
|
+
const choices = new Map();
|
|
234
|
+
for (const c of chunks) {
|
|
235
|
+
for (const ch of c.choices ?? []) {
|
|
236
|
+
const i = ch.index ?? 0;
|
|
237
|
+
const cur = choices.get(i) ?? { index: i, message: { role: 'assistant', content: '' }, finish_reason: null };
|
|
238
|
+
if (ch.delta?.content)
|
|
239
|
+
cur.message.content += ch.delta.content;
|
|
240
|
+
if (ch.delta?.tool_calls)
|
|
241
|
+
cur.message.tool_calls = [...(cur.message.tool_calls ?? []), ...ch.delta.tool_calls];
|
|
242
|
+
if (ch.finish_reason)
|
|
243
|
+
cur.finish_reason = ch.finish_reason;
|
|
244
|
+
choices.set(i, cur);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
const first = chunks[0] ?? {};
|
|
248
|
+
return {
|
|
249
|
+
id: first.id ?? `chatcmpl-${Date.now()}`,
|
|
250
|
+
object: 'chat.completion',
|
|
251
|
+
created: first.created ?? Math.floor(Date.now() / 1000),
|
|
252
|
+
model,
|
|
253
|
+
choices: [...choices.values()].sort((a, b) => a.index - b.index),
|
|
254
|
+
usage: { prompt_tokens: prompt, completion_tokens: completion, total_tokens: prompt + completion },
|
|
255
|
+
};
|
|
256
|
+
}
|
package/dist/seal.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Private route: the router encrypts the request to the enclave key the provider attested on chain (X25519 +
|
|
2
|
+
// HKDF-SHA256 + AES-256-GCM). Only code inside that enclave holds the private half, so the operator's host, the
|
|
3
|
+
// network and this project see ciphertext. Replies come back sealed under the same derived key.
|
|
4
|
+
import { createCipheriv, createDecipheriv, createPrivateKey, createPublicKey, diffieHellman, generateKeyPairSync, hkdfSync, randomBytes } from 'node:crypto';
|
|
5
|
+
const SPKI_X25519 = Buffer.from('302a300506032b656e032100', 'hex');
|
|
6
|
+
const PKCS8_X25519 = Buffer.from('302e020100300506032b656e04220420', 'hex');
|
|
7
|
+
const INFO = Buffer.from('orrery/seal/1');
|
|
8
|
+
export function newEnclaveKey() {
|
|
9
|
+
const { publicKey, privateKey } = generateKeyPairSync('x25519');
|
|
10
|
+
const pub = publicKey.export({ format: 'der', type: 'spki' }).subarray(-32);
|
|
11
|
+
const secret = privateKey.export({ format: 'der', type: 'pkcs8' }).subarray(-32);
|
|
12
|
+
return { publicKey: Buffer.from(pub), privateKey, secret: Buffer.from(secret) };
|
|
13
|
+
}
|
|
14
|
+
export function enclaveKeyFromSecret(secret) {
|
|
15
|
+
const privateKey = createPrivateKey({ key: Buffer.concat([PKCS8_X25519, secret]), format: 'der', type: 'pkcs8' });
|
|
16
|
+
const pub = createPublicKey(privateKey).export({ format: 'der', type: 'spki' }).subarray(-32);
|
|
17
|
+
return { publicKey: Buffer.from(pub), privateKey, secret };
|
|
18
|
+
}
|
|
19
|
+
const publicFromRaw = (raw) => createPublicKey({ key: Buffer.concat([SPKI_X25519, raw]), format: 'der', type: 'spki' });
|
|
20
|
+
function derive(shared, epk, recipient) {
|
|
21
|
+
return Buffer.from(hkdfSync('sha256', shared, Buffer.concat([epk, recipient]), INFO, 32));
|
|
22
|
+
}
|
|
23
|
+
export function encrypt(key, plaintext) {
|
|
24
|
+
const iv = randomBytes(12);
|
|
25
|
+
const c = createCipheriv('aes-256-gcm', key, iv);
|
|
26
|
+
const ct = Buffer.concat([c.update(plaintext), c.final()]);
|
|
27
|
+
return Buffer.concat([iv, ct, c.getAuthTag()]);
|
|
28
|
+
}
|
|
29
|
+
export function decrypt(key, box) {
|
|
30
|
+
if (box.length < 28)
|
|
31
|
+
throw new Error('sealed box too short');
|
|
32
|
+
const d = createDecipheriv('aes-256-gcm', key, box.subarray(0, 12));
|
|
33
|
+
d.setAuthTag(box.subarray(box.length - 16));
|
|
34
|
+
return Buffer.concat([d.update(box.subarray(12, box.length - 16)), d.final()]);
|
|
35
|
+
}
|
|
36
|
+
/** Router side: seal a request to an enclave. Returns the wire body and the key that will open the reply. */
|
|
37
|
+
export function sealRequest(enclavePublic, plaintext) {
|
|
38
|
+
const eph = newEnclaveKey();
|
|
39
|
+
const shared = diffieHellman({ privateKey: eph.privateKey, publicKey: publicFromRaw(enclavePublic) });
|
|
40
|
+
const key = derive(shared, eph.publicKey, enclavePublic);
|
|
41
|
+
return { body: JSON.stringify({ sealed: 1, epk: eph.publicKey.toString('base64'), box: encrypt(key, plaintext).toString('base64') }), key };
|
|
42
|
+
}
|
|
43
|
+
/** Enclave side: open a sealed request. Returns the plaintext and the key to seal the reply with. */
|
|
44
|
+
export function openRequest(enclave, wire) {
|
|
45
|
+
const epk = Buffer.from(wire.epk, 'base64');
|
|
46
|
+
const shared = diffieHellman({ privateKey: enclave.privateKey, publicKey: publicFromRaw(epk) });
|
|
47
|
+
const key = derive(shared, epk, enclave.publicKey);
|
|
48
|
+
return { plaintext: decrypt(key, Buffer.from(wire.box, 'base64')), key };
|
|
49
|
+
}
|
package/dist/sse.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export async function* readSse(body) {
|
|
2
|
+
const decoder = new TextDecoder();
|
|
3
|
+
let buf = '';
|
|
4
|
+
let event = null;
|
|
5
|
+
let data = [];
|
|
6
|
+
for await (const chunk of body) {
|
|
7
|
+
buf += decoder.decode(chunk, { stream: true });
|
|
8
|
+
let nl;
|
|
9
|
+
while ((nl = buf.indexOf('\n')) >= 0) {
|
|
10
|
+
const line = buf.slice(0, nl).replace(/\r$/, '');
|
|
11
|
+
buf = buf.slice(nl + 1);
|
|
12
|
+
if (line === '') {
|
|
13
|
+
if (data.length)
|
|
14
|
+
yield { event, data: data.join('\n') };
|
|
15
|
+
event = null;
|
|
16
|
+
data = [];
|
|
17
|
+
}
|
|
18
|
+
else if (line.startsWith('event:')) {
|
|
19
|
+
event = line.slice(6).trim();
|
|
20
|
+
}
|
|
21
|
+
else if (line.startsWith('data:')) {
|
|
22
|
+
data.push(line.slice(5).replace(/^ /, ''));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (data.length)
|
|
27
|
+
yield { event, data: data.join('\n') };
|
|
28
|
+
}
|
|
29
|
+
export function sseFrame(data, event) {
|
|
30
|
+
return `${event ? `event: ${event}\n` : ''}data: ${data}\n\n`;
|
|
31
|
+
}
|
|
32
|
+
export async function fetchWithTimeout(url, init = {}) {
|
|
33
|
+
const ctl = new AbortController();
|
|
34
|
+
const t = setTimeout(() => ctl.abort(new Error(`timeout after ${init.timeoutMs ?? 10_000}ms`)), init.timeoutMs ?? 10_000);
|
|
35
|
+
try {
|
|
36
|
+
return await fetch(url, { ...init, signal: init.signal ?? ctl.signal });
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
clearTimeout(t);
|
|
40
|
+
}
|
|
41
|
+
}
|
package/dist/usage.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Each paid request extends a hash chain over what it cost and why. The router signs the chain head inside every
|
|
2
|
+
// voucher and the program stores the head it redeems, so either side can later prove which requests a payment covered.
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
export const ZERO_HASH = Buffer.alloc(32);
|
|
5
|
+
export function nextUsageHash(prev, r) {
|
|
6
|
+
return createHash('sha256').update(prev).update(`${r.requestId}|${r.model}|${r.inputTokens}|${r.outputTokens}|${r.cost}`).digest();
|
|
7
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,67 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "orrery-node",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Peer-to-peer AI inference on Solana. An OpenAI-compatible router on localhost that sends every request straight to a provider's machine and pays it per answer through a payment channel. Zero cut in between.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"solana",
|
|
8
|
+
"ai",
|
|
9
|
+
"llm",
|
|
10
|
+
"inference",
|
|
11
|
+
"p2p",
|
|
12
|
+
"peer-to-peer",
|
|
13
|
+
"openai",
|
|
14
|
+
"openai-compatible",
|
|
15
|
+
"router",
|
|
16
|
+
"gateway",
|
|
17
|
+
"payment-channel",
|
|
18
|
+
"micropayments",
|
|
19
|
+
"usdc",
|
|
20
|
+
"decentralized",
|
|
21
|
+
"vllm",
|
|
22
|
+
"ollama",
|
|
23
|
+
"gpu",
|
|
24
|
+
"cursor",
|
|
25
|
+
"langchain"
|
|
26
|
+
],
|
|
27
|
+
"homepage": "https://orrerynode.com",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/orrery-node/orrery.git",
|
|
31
|
+
"directory": "node"
|
|
32
|
+
},
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/orrery-node/orrery/issues"
|
|
35
|
+
},
|
|
36
|
+
"type": "module",
|
|
37
|
+
"bin": {
|
|
38
|
+
"orrery": "./dist/cli.js"
|
|
39
|
+
},
|
|
40
|
+
"files": [
|
|
41
|
+
"dist",
|
|
42
|
+
"README.md",
|
|
43
|
+
"LICENSE"
|
|
44
|
+
],
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=22"
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"orrery": "tsx src/cli.ts",
|
|
50
|
+
"test": "tsx --test test/*.test.ts",
|
|
51
|
+
"typecheck": "tsc --noEmit",
|
|
52
|
+
"e2e": "bash devtools/e2e.sh",
|
|
53
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json && chmod +x dist/cli.js",
|
|
54
|
+
"prepublishOnly": "pnpm run build && pnpm test"
|
|
55
|
+
},
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"@solana/spl-token": "0.4.14",
|
|
58
|
+
"@solana/web3.js": "1.98.4",
|
|
59
|
+
"bs58": "6.0.0"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@types/node": "22.19.1",
|
|
63
|
+
"tsx": "4.23.13",
|
|
64
|
+
"typescript": "5.9.3"
|
|
65
|
+
},
|
|
66
|
+
"author": "Orrery (https://orrerynode.com)"
|
|
67
|
+
}
|