technocore-compute 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 +21 -0
- package/README.md +156 -0
- package/dist/cli.d.ts +11 -0
- package/dist/cli.js +114 -0
- package/dist/exchange.d.ts +76 -0
- package/dist/exchange.js +262 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.js +52 -0
- package/dist/server.d.ts +26 -0
- package/dist/server.js +205 -0
- package/dist/types.d.ts +110 -0
- package/dist/types.js +7 -0
- package/package.json +40 -0
- package/src/cli.ts +120 -0
- package/src/exchange.ts +308 -0
- package/src/index.ts +63 -0
- package/src/server.ts +234 -0
- package/src/types.ts +124 -0
- package/test/exchange.test.ts +218 -0
- package/tsconfig.json +18 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for the Technocore Compute Exchange
|
|
3
|
+
*
|
|
4
|
+
* Agents can offer compute resources and other agents can rent them,
|
|
5
|
+
* with payments handled via tclk HTLC/PTLC.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Compute hardware type */
|
|
9
|
+
export type ComputeType = 'gpu' | 'cpu' | 'tpu' | 'npu' | 'any';
|
|
10
|
+
|
|
11
|
+
/** Job status */
|
|
12
|
+
export type JobStatus =
|
|
13
|
+
| 'listed' // Provider advertised compute
|
|
14
|
+
| 'requested' // Consumer wants compute
|
|
15
|
+
| 'matched' // Provider accepted the request
|
|
16
|
+
| 'locked' // Payment locked via tclk
|
|
17
|
+
| 'running' // Compute job is executing
|
|
18
|
+
| 'completed' // Job finished successfully
|
|
19
|
+
| 'failed' // Job failed
|
|
20
|
+
| 'refunded'; // Payment refunded
|
|
21
|
+
|
|
22
|
+
/** Provider advertising compute resources */
|
|
23
|
+
export interface ComputeProvider {
|
|
24
|
+
/** Provider DID */
|
|
25
|
+
did: string;
|
|
26
|
+
/** Display name */
|
|
27
|
+
name: string;
|
|
28
|
+
/** Hardware available */
|
|
29
|
+
hardware: ComputeType;
|
|
30
|
+
/** GPU model if GPU (e.g. "A100", "H100", "RTX4090") */
|
|
31
|
+
gpuModel?: string;
|
|
32
|
+
/** VRAM in GB */
|
|
33
|
+
vramGB?: number;
|
|
34
|
+
/** CPU cores */
|
|
35
|
+
cpuCores?: number;
|
|
36
|
+
/** RAM in GB */
|
|
37
|
+
ramGB?: number;
|
|
38
|
+
/** Storage in GB */
|
|
39
|
+
storageGB?: number;
|
|
40
|
+
/** Price per hour in smallest FLOP unit */
|
|
41
|
+
pricePerHour: string;
|
|
42
|
+
/** Minimum job duration in seconds */
|
|
43
|
+
minDurationSec: number;
|
|
44
|
+
/** Maximum job duration in seconds */
|
|
45
|
+
maxDurationSec: number;
|
|
46
|
+
/** Available now */
|
|
47
|
+
available: boolean;
|
|
48
|
+
/** Average rating (0-5) */
|
|
49
|
+
rating: number;
|
|
50
|
+
/** Total jobs completed */
|
|
51
|
+
jobsCompleted: number;
|
|
52
|
+
/** technocore room for coordination */
|
|
53
|
+
room: string;
|
|
54
|
+
/** When this was registered */
|
|
55
|
+
registeredAt: number;
|
|
56
|
+
/** Last heartbeat */
|
|
57
|
+
lastHeartbeat: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Consumer requesting compute */
|
|
61
|
+
export interface ComputeRequest {
|
|
62
|
+
/** Request ID */
|
|
63
|
+
id: string;
|
|
64
|
+
/** Consumer DID */
|
|
65
|
+
consumerDid: string;
|
|
66
|
+
/** Hardware needed */
|
|
67
|
+
hardware: ComputeType;
|
|
68
|
+
/** Minimum GPU model */
|
|
69
|
+
minGpuModel?: string;
|
|
70
|
+
/** Minimum VRAM needed */
|
|
71
|
+
minVramGB?: number;
|
|
72
|
+
/** Minimum CPU cores */
|
|
73
|
+
minCpuCores?: number;
|
|
74
|
+
/** Minimum RAM needed */
|
|
75
|
+
minRamGB?: number;
|
|
76
|
+
/** Duration needed in seconds */
|
|
77
|
+
durationSec: number;
|
|
78
|
+
/** Max price willing to pay */
|
|
79
|
+
maxPricePerHour: string;
|
|
80
|
+
/** Job description */
|
|
81
|
+
description: string;
|
|
82
|
+
/** When requested */
|
|
83
|
+
createdAt: number;
|
|
84
|
+
/** Expiry timestamp */
|
|
85
|
+
expiresAt: number;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Matched job between provider and consumer */
|
|
89
|
+
export interface ComputeJob {
|
|
90
|
+
/** Job ID */
|
|
91
|
+
id: string;
|
|
92
|
+
/** Provider DID */
|
|
93
|
+
providerDid: string;
|
|
94
|
+
/** Consumer DID */
|
|
95
|
+
consumerDid: string;
|
|
96
|
+
/** Matched request */
|
|
97
|
+
request: ComputeRequest;
|
|
98
|
+
/** Provider used */
|
|
99
|
+
provider: ComputeProvider;
|
|
100
|
+
/** Current status */
|
|
101
|
+
status: JobStatus;
|
|
102
|
+
/** tclk contract ID for payment */
|
|
103
|
+
tclkContractId?: string;
|
|
104
|
+
/** Total cost */
|
|
105
|
+
totalCost: string;
|
|
106
|
+
/** Room where deal is coordinated */
|
|
107
|
+
room: string;
|
|
108
|
+
/** Created timestamp */
|
|
109
|
+
createdAt: number;
|
|
110
|
+
/** Started timestamp */
|
|
111
|
+
startedAt?: number;
|
|
112
|
+
/** Completed timestamp */
|
|
113
|
+
completedAt?: number;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Compute exchange stats */
|
|
117
|
+
export interface ExchangeStats {
|
|
118
|
+
totalProviders: number;
|
|
119
|
+
availableProviders: number;
|
|
120
|
+
totalJobs: number;
|
|
121
|
+
activeJobs: number;
|
|
122
|
+
completedJobs: number;
|
|
123
|
+
totalVolume: string;
|
|
124
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { ComputeExchange } from '../src/exchange.js';
|
|
4
|
+
|
|
5
|
+
const SAMPLE_PROVIDER = {
|
|
6
|
+
did: 'did:key:z6MkTestProvider1234567890123456789012345678',
|
|
7
|
+
name: 'GPU Farm',
|
|
8
|
+
hardware: 'gpu' as const,
|
|
9
|
+
gpuModel: 'A100',
|
|
10
|
+
vramGB: 80,
|
|
11
|
+
pricePerHour: '5000000',
|
|
12
|
+
minDurationSec: 300,
|
|
13
|
+
maxDurationSec: 86400,
|
|
14
|
+
available: true,
|
|
15
|
+
rating: 4.8,
|
|
16
|
+
jobsCompleted: 42,
|
|
17
|
+
room: 'compute-gpu',
|
|
18
|
+
registeredAt: Date.now(),
|
|
19
|
+
lastHeartbeat: Date.now(),
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const SAMPLE_REQUEST = {
|
|
23
|
+
consumerDid: 'did:key:z6MkTestConsumer12345678901234567890123456',
|
|
24
|
+
hardware: 'gpu' as const,
|
|
25
|
+
minVramGB: 24,
|
|
26
|
+
durationSec: 3600,
|
|
27
|
+
maxPricePerHour: '10000000',
|
|
28
|
+
description: 'Train a small model',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
describe('ComputeExchange', () => {
|
|
32
|
+
describe('providers', () => {
|
|
33
|
+
it('registers a provider', () => {
|
|
34
|
+
const exchange = new ComputeExchange();
|
|
35
|
+
exchange.registerProvider(SAMPLE_PROVIDER);
|
|
36
|
+
const providers = exchange.listProviders();
|
|
37
|
+
assert.strictEqual(providers.length, 1);
|
|
38
|
+
assert.strictEqual(providers[0].did, SAMPLE_PROVIDER.did);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('finds providers by hardware type', () => {
|
|
42
|
+
const exchange = new ComputeExchange();
|
|
43
|
+
exchange.registerProvider({ ...SAMPLE_PROVIDER, hardware: 'gpu' });
|
|
44
|
+
exchange.registerProvider({ ...SAMPLE_PROVIDER, did: 'did:key:z6MkCpu123', hardware: 'cpu', cpuCores: 64, ramGB: 128 });
|
|
45
|
+
|
|
46
|
+
const gpuProviders = exchange.findProviders({ hardware: 'gpu' });
|
|
47
|
+
assert.strictEqual(gpuProviders.length, 1);
|
|
48
|
+
assert.strictEqual(gpuProviders[0].hardware, 'gpu');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('finds providers by VRAM', () => {
|
|
52
|
+
const exchange = new ComputeExchange();
|
|
53
|
+
exchange.registerProvider({ ...SAMPLE_PROVIDER, vramGB: 16 });
|
|
54
|
+
exchange.registerProvider({ ...SAMPLE_PROVIDER, did: 'did:key:z6MkBig123', vramGB: 80 });
|
|
55
|
+
|
|
56
|
+
const providers = exchange.findProviders({ hardware: 'gpu', minVramGB: 24 });
|
|
57
|
+
assert.strictEqual(providers.length, 1);
|
|
58
|
+
assert.strictEqual(providers[0].vramGB, 80);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('finds providers by max price', () => {
|
|
62
|
+
const exchange = new ComputeExchange();
|
|
63
|
+
exchange.registerProvider({ ...SAMPLE_PROVIDER, pricePerHour: '5000000' });
|
|
64
|
+
exchange.registerProvider({ ...SAMPLE_PROVIDER, did: 'did:key:z6MkExp123', pricePerHour: '50000000' });
|
|
65
|
+
|
|
66
|
+
const providers = exchange.findProviders({ maxPricePerHour: '10000000' });
|
|
67
|
+
assert.strictEqual(providers.length, 1);
|
|
68
|
+
assert.strictEqual(providers[0].pricePerHour, '5000000');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('skips unavailable providers', () => {
|
|
72
|
+
const exchange = new ComputeExchange();
|
|
73
|
+
exchange.registerProvider({ ...SAMPLE_PROVIDER, available: false });
|
|
74
|
+
|
|
75
|
+
const providers = exchange.findProviders({ hardware: 'gpu' });
|
|
76
|
+
assert.strictEqual(providers.length, 0);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('updates heartbeat', () => {
|
|
80
|
+
const exchange = new ComputeExchange();
|
|
81
|
+
exchange.registerProvider({ ...SAMPLE_PROVIDER, available: false });
|
|
82
|
+
exchange.heartbeat(SAMPLE_PROVIDER.did);
|
|
83
|
+
|
|
84
|
+
const provider = exchange.getProvider(SAMPLE_PROVIDER.did);
|
|
85
|
+
assert(provider?.available);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('prunes stale providers', () => {
|
|
89
|
+
const exchange = new ComputeExchange();
|
|
90
|
+
exchange.registerProvider({
|
|
91
|
+
...SAMPLE_PROVIDER,
|
|
92
|
+
lastHeartbeat: Date.now() - 600_000, // 10 minutes ago
|
|
93
|
+
});
|
|
94
|
+
exchange.registerProvider({
|
|
95
|
+
...SAMPLE_PROVIDER,
|
|
96
|
+
did: 'did:key:z6MkFresh1234567890123456789012345678',
|
|
97
|
+
lastHeartbeat: Date.now(),
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const pruned = exchange.pruneStaleProviders(300_000);
|
|
101
|
+
assert.strictEqual(pruned, 1);
|
|
102
|
+
assert.strictEqual(exchange.listProviders().length, 1);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe('requests', () => {
|
|
107
|
+
it('creates a request', () => {
|
|
108
|
+
const exchange = new ComputeExchange();
|
|
109
|
+
const request = exchange.createRequest(SAMPLE_REQUEST);
|
|
110
|
+
assert(request.id.startsWith('req-'));
|
|
111
|
+
assert.strictEqual(request.consumerDid, SAMPLE_REQUEST.consumerDid);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('lists pending requests', () => {
|
|
115
|
+
const exchange = new ComputeExchange();
|
|
116
|
+
exchange.createRequest(SAMPLE_REQUEST);
|
|
117
|
+
const pending = exchange.listPendingRequests();
|
|
118
|
+
assert.strictEqual(pending.length, 1);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('prunes expired requests', () => {
|
|
122
|
+
const exchange = new ComputeExchange();
|
|
123
|
+
exchange.createRequest({ ...SAMPLE_REQUEST, durationSec: -1 }); // Already expired
|
|
124
|
+
const pruned = exchange.pruneExpiredRequests();
|
|
125
|
+
assert.strictEqual(pruned, 1);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe('jobs', () => {
|
|
130
|
+
it('matches a request with a provider', () => {
|
|
131
|
+
const exchange = new ComputeExchange();
|
|
132
|
+
exchange.registerProvider(SAMPLE_PROVIDER);
|
|
133
|
+
const request = exchange.createRequest(SAMPLE_REQUEST);
|
|
134
|
+
const job = exchange.matchRequest(request.id);
|
|
135
|
+
|
|
136
|
+
assert(job !== null);
|
|
137
|
+
assert.strictEqual(job!.providerDid, SAMPLE_PROVIDER.did);
|
|
138
|
+
assert.strictEqual(job!.consumerDid, SAMPLE_REQUEST.consumerDid);
|
|
139
|
+
assert.strictEqual(job!.status, 'matched');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('returns null when no provider matches', () => {
|
|
143
|
+
const exchange = new ComputeExchange();
|
|
144
|
+
const request = exchange.createRequest({
|
|
145
|
+
...SAMPLE_REQUEST,
|
|
146
|
+
hardware: 'tpu', // No TPU providers
|
|
147
|
+
});
|
|
148
|
+
const job = exchange.matchRequest(request.id);
|
|
149
|
+
assert.strictEqual(job, null);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('picks cheapest provider', () => {
|
|
153
|
+
const exchange = new ComputeExchange();
|
|
154
|
+
exchange.registerProvider({ ...SAMPLE_PROVIDER, pricePerHour: '5000000' });
|
|
155
|
+
exchange.registerProvider({
|
|
156
|
+
...SAMPLE_PROVIDER,
|
|
157
|
+
did: 'did:key:z6MkCheap12345678901234567890123456',
|
|
158
|
+
pricePerHour: '1000000',
|
|
159
|
+
});
|
|
160
|
+
const request = exchange.createRequest(SAMPLE_REQUEST);
|
|
161
|
+
const job = exchange.matchRequest(request.id);
|
|
162
|
+
|
|
163
|
+
assert(job !== null);
|
|
164
|
+
assert.strictEqual(job!.providerDid, 'did:key:z6MkCheap12345678901234567890123456');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('transitions job through states', () => {
|
|
168
|
+
const exchange = new ComputeExchange();
|
|
169
|
+
exchange.registerProvider(SAMPLE_PROVIDER);
|
|
170
|
+
const request = exchange.createRequest(SAMPLE_REQUEST);
|
|
171
|
+
const job = exchange.matchRequest(request.id)!;
|
|
172
|
+
|
|
173
|
+
assert(exchange.acceptJob(job.id));
|
|
174
|
+
assert.strictEqual(exchange.getJob(job.id)!.status, 'locked');
|
|
175
|
+
|
|
176
|
+
assert(exchange.startJob(job.id));
|
|
177
|
+
assert.strictEqual(exchange.getJob(job.id)!.status, 'running');
|
|
178
|
+
|
|
179
|
+
assert(exchange.completeJob(job.id));
|
|
180
|
+
assert.strictEqual(exchange.getJob(job.id)!.status, 'completed');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('refunds a job', () => {
|
|
184
|
+
const exchange = new ComputeExchange();
|
|
185
|
+
exchange.registerProvider(SAMPLE_PROVIDER);
|
|
186
|
+
const request = exchange.createRequest(SAMPLE_REQUEST);
|
|
187
|
+
const job = exchange.matchRequest(request.id)!;
|
|
188
|
+
|
|
189
|
+
assert(exchange.refundJob(job.id));
|
|
190
|
+
assert.strictEqual(exchange.getJob(job.id)!.status, 'refunded');
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('updates provider stats on completion', () => {
|
|
194
|
+
const exchange = new ComputeExchange();
|
|
195
|
+
exchange.registerProvider({ ...SAMPLE_PROVIDER, jobsCompleted: 10 });
|
|
196
|
+
const request = exchange.createRequest(SAMPLE_REQUEST);
|
|
197
|
+
const job = exchange.matchRequest(request.id)!;
|
|
198
|
+
exchange.acceptJob(job.id);
|
|
199
|
+
exchange.startJob(job.id);
|
|
200
|
+
exchange.completeJob(job.id);
|
|
201
|
+
|
|
202
|
+
const provider = exchange.getProvider(SAMPLE_PROVIDER.did);
|
|
203
|
+
assert.strictEqual(provider!.jobsCompleted, 11);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe('stats', () => {
|
|
208
|
+
it('returns correct stats', () => {
|
|
209
|
+
const exchange = new ComputeExchange();
|
|
210
|
+
exchange.registerProvider(SAMPLE_PROVIDER);
|
|
211
|
+
exchange.registerProvider({ ...SAMPLE_PROVIDER, did: 'did:key:z6MkBusy1234567890123456789012345', available: false });
|
|
212
|
+
|
|
213
|
+
const stats = exchange.getStats();
|
|
214
|
+
assert.strictEqual(stats.totalProviders, 2);
|
|
215
|
+
assert.strictEqual(stats.availableProviders, 1);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ES2022",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"outDir": "./dist",
|
|
8
|
+
"rootDir": "./src",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"resolveJsonModule": true,
|
|
14
|
+
"isolatedModules": true
|
|
15
|
+
},
|
|
16
|
+
"include": ["src/**/*"],
|
|
17
|
+
"exclude": ["node_modules", "dist", "test"]
|
|
18
|
+
}
|