@tangle-network/tcloud 0.1.4 → 0.3.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/README.md +69 -10
- package/dist/chunk-ADGH2R2R.js +263 -0
- package/dist/chunk-LN7XX5KG.js +1176 -0
- package/dist/{chunk-PILYAKCF.js → chunk-W7PUZZRE.js} +4 -2
- package/dist/cli.cjs +969 -193
- package/dist/cli.js +141 -26
- package/dist/client-DskWX_BT.d.cts +1117 -0
- package/dist/client-DskWX_BT.d.ts +1117 -0
- package/dist/index.cjs +835 -169
- package/dist/index.d.cts +5 -2
- package/dist/index.d.ts +5 -2
- package/dist/index.js +10 -4
- package/dist/instance.cjs +1434 -0
- package/dist/instance.d.cts +110 -0
- package/dist/instance.d.ts +110 -0
- package/dist/instance.js +229 -0
- package/dist/shielded.cjs +831 -169
- package/dist/shielded.d.cts +61 -2
- package/dist/shielded.d.ts +61 -2
- package/dist/shielded.js +2 -1
- package/package.json +15 -2
- package/dist/chunk-KVXWEAK3.js +0 -771
- package/dist/shielded-BTi_OftW.d.cts +0 -513
- package/dist/shielded-BTi_OftW.d.ts +0 -513
package/dist/index.cjs
CHANGED
|
@@ -30,6 +30,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
+
BridgeSession: () => BridgeSession,
|
|
34
|
+
PrivateRouter: () => PrivateRouter,
|
|
33
35
|
TCloud: () => TCloud,
|
|
34
36
|
TCloudClient: () => TCloudClient,
|
|
35
37
|
TCloudError: () => TCloudError,
|
|
@@ -40,6 +42,219 @@ __export(index_exports, {
|
|
|
40
42
|
});
|
|
41
43
|
module.exports = __toCommonJS(index_exports);
|
|
42
44
|
|
|
45
|
+
// src/private-router.ts
|
|
46
|
+
function secureRandom() {
|
|
47
|
+
const arr = new Uint32Array(1);
|
|
48
|
+
crypto.getRandomValues(arr);
|
|
49
|
+
return arr[0] / (4294967295 + 1);
|
|
50
|
+
}
|
|
51
|
+
var PrivateRouter = class {
|
|
52
|
+
config;
|
|
53
|
+
operators = [];
|
|
54
|
+
usage = /* @__PURE__ */ new Map();
|
|
55
|
+
currentIndex = 0;
|
|
56
|
+
totalRequests = 0;
|
|
57
|
+
constructor(config = {}) {
|
|
58
|
+
this.config = {
|
|
59
|
+
strategy: config.strategy || "round-robin",
|
|
60
|
+
maxRequestsPerOperator: config.maxRequestsPerOperator || 5,
|
|
61
|
+
minOperators: config.minOperators || 3,
|
|
62
|
+
preferRegions: config.preferRegions,
|
|
63
|
+
excludeOperators: config.excludeOperators,
|
|
64
|
+
summarizeOnSwitch: config.summarizeOnSwitch ?? false
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/** Set the available operator pool */
|
|
68
|
+
setOperators(operators) {
|
|
69
|
+
let filtered = operators.filter(
|
|
70
|
+
(o) => !this.config.excludeOperators?.includes(o.slug)
|
|
71
|
+
);
|
|
72
|
+
if (this.config.preferRegions?.length) {
|
|
73
|
+
filtered.sort((a, b) => {
|
|
74
|
+
const aPreferred = this.config.preferRegions.includes(a.region) ? 0 : 1;
|
|
75
|
+
const bPreferred = this.config.preferRegions.includes(b.region) ? 0 : 1;
|
|
76
|
+
return aPreferred - bPreferred;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
this.operators = filtered;
|
|
80
|
+
}
|
|
81
|
+
/** Select the next operator for a request */
|
|
82
|
+
selectOperator(model) {
|
|
83
|
+
const eligible = this.operators.filter((o) => o.models.includes(model));
|
|
84
|
+
if (eligible.length === 0) return null;
|
|
85
|
+
if (eligible.length < this.config.minOperators) {
|
|
86
|
+
console.warn(
|
|
87
|
+
`[PrivateRouter] Only ${eligible.length} eligible operator(s) for model "${model}", but minOperators requires ${this.config.minOperators}. Refusing to route.`
|
|
88
|
+
);
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
this.totalRequests++;
|
|
92
|
+
switch (this.config.strategy) {
|
|
93
|
+
case "round-robin":
|
|
94
|
+
return this.roundRobin(eligible);
|
|
95
|
+
case "random":
|
|
96
|
+
return this.random(eligible);
|
|
97
|
+
case "geo-distributed":
|
|
98
|
+
return this.geoDistributed(eligible);
|
|
99
|
+
case "min-exposure":
|
|
100
|
+
return this.minExposure(eligible);
|
|
101
|
+
case "latency-aware":
|
|
102
|
+
return this.latencyAware(eligible);
|
|
103
|
+
default:
|
|
104
|
+
return this.roundRobin(eligible);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/** Should we summarize context before this request? (operator is changing) */
|
|
108
|
+
shouldSummarize(model) {
|
|
109
|
+
if (!this.config.summarizeOnSwitch) return false;
|
|
110
|
+
const next = this.peekNextOperator(model);
|
|
111
|
+
const last = this.getLastUsedOperator();
|
|
112
|
+
return next !== null && last !== null && next.slug !== last.slug;
|
|
113
|
+
}
|
|
114
|
+
/** Get privacy stats */
|
|
115
|
+
getStats() {
|
|
116
|
+
return {
|
|
117
|
+
totalRequests: this.totalRequests,
|
|
118
|
+
operatorsUsed: this.usage.size,
|
|
119
|
+
operatorBreakdown: Array.from(this.usage.values()).map((u) => ({
|
|
120
|
+
slug: u.slug,
|
|
121
|
+
requests: u.requestCount,
|
|
122
|
+
lastUsed: u.lastUsedAt
|
|
123
|
+
})),
|
|
124
|
+
strategy: this.config.strategy
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
// ─── Strategies ────────────────────────────────────────────
|
|
128
|
+
roundRobin(eligible) {
|
|
129
|
+
const op = eligible[this.currentIndex % eligible.length];
|
|
130
|
+
this.currentIndex++;
|
|
131
|
+
this.recordUsage(op);
|
|
132
|
+
return op;
|
|
133
|
+
}
|
|
134
|
+
random(eligible) {
|
|
135
|
+
const idx = Math.floor(secureRandom() * eligible.length);
|
|
136
|
+
const op = eligible[idx];
|
|
137
|
+
this.recordUsage(op);
|
|
138
|
+
return op;
|
|
139
|
+
}
|
|
140
|
+
geoDistributed(eligible) {
|
|
141
|
+
const regionUsage = /* @__PURE__ */ new Map();
|
|
142
|
+
for (const op2 of eligible) {
|
|
143
|
+
const usage = this.usage.get(op2.slug)?.requestCount || 0;
|
|
144
|
+
const current = regionUsage.get(op2.region) || 0;
|
|
145
|
+
regionUsage.set(op2.region, current + usage);
|
|
146
|
+
}
|
|
147
|
+
const sortedRegions = [...regionUsage.entries()].sort((a, b) => a[1] - b[1]);
|
|
148
|
+
const targetRegion = sortedRegions[0]?.[0];
|
|
149
|
+
const regionOps = eligible.filter((o) => o.region === targetRegion);
|
|
150
|
+
const op = regionOps[Math.floor(secureRandom() * regionOps.length)] || eligible[0];
|
|
151
|
+
this.recordUsage(op);
|
|
152
|
+
return op;
|
|
153
|
+
}
|
|
154
|
+
minExposure(eligible) {
|
|
155
|
+
const lastUsed = this.getLastUsedOperator();
|
|
156
|
+
if (lastUsed) {
|
|
157
|
+
const lastUsage = this.usage.get(lastUsed.slug);
|
|
158
|
+
const others = eligible.filter((o) => o.slug !== lastUsed.slug);
|
|
159
|
+
if (others.length > 0 && lastUsage && lastUsage.requestCount > 0) {
|
|
160
|
+
const sorted2 = others.sort(
|
|
161
|
+
(a, b) => (this.usage.get(a.slug)?.requestCount || 0) - (this.usage.get(b.slug)?.requestCount || 0)
|
|
162
|
+
);
|
|
163
|
+
const op2 = sorted2[0];
|
|
164
|
+
this.recordUsage(op2);
|
|
165
|
+
return op2;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const sorted = [...eligible].sort(
|
|
169
|
+
(a, b) => (this.usage.get(a.slug)?.requestCount || 0) - (this.usage.get(b.slug)?.requestCount || 0)
|
|
170
|
+
);
|
|
171
|
+
const op = sorted[0];
|
|
172
|
+
this.recordUsage(op);
|
|
173
|
+
return op;
|
|
174
|
+
}
|
|
175
|
+
latencyAware(eligible) {
|
|
176
|
+
const weights = eligible.map((o) => {
|
|
177
|
+
const latencyWeight = 1 / Math.max(o.avgLatencyMs, 10);
|
|
178
|
+
const usagePenalty = (this.usage.get(o.slug)?.requestCount || 0) * 0.1;
|
|
179
|
+
return Math.max(latencyWeight - usagePenalty, 0.01);
|
|
180
|
+
});
|
|
181
|
+
const totalWeight = weights.reduce((s, w) => s + w, 0);
|
|
182
|
+
let r = secureRandom() * totalWeight;
|
|
183
|
+
for (let i = 0; i < eligible.length; i++) {
|
|
184
|
+
r -= weights[i];
|
|
185
|
+
if (r <= 0) {
|
|
186
|
+
this.recordUsage(eligible[i]);
|
|
187
|
+
return eligible[i];
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const op = eligible[eligible.length - 1];
|
|
191
|
+
this.recordUsage(op);
|
|
192
|
+
return op;
|
|
193
|
+
}
|
|
194
|
+
// ─── Helpers ───────────────────────────────────────────────
|
|
195
|
+
recordUsage(op) {
|
|
196
|
+
const existing = this.usage.get(op.slug);
|
|
197
|
+
this.usage.set(op.slug, {
|
|
198
|
+
slug: op.slug,
|
|
199
|
+
requestCount: (existing?.requestCount || 0) + 1,
|
|
200
|
+
lastUsedAt: Date.now()
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
getLastUsedOperator() {
|
|
204
|
+
let latest = null;
|
|
205
|
+
for (const u of this.usage.values()) {
|
|
206
|
+
if (!latest || u.lastUsedAt > latest.lastUsedAt) latest = u;
|
|
207
|
+
}
|
|
208
|
+
if (!latest) return null;
|
|
209
|
+
return this.operators.find((o) => o.slug === latest.slug) || null;
|
|
210
|
+
}
|
|
211
|
+
peekNextOperator(model) {
|
|
212
|
+
const eligible = this.operators.filter((o) => o.models.includes(model));
|
|
213
|
+
if (eligible.length === 0) return null;
|
|
214
|
+
if (eligible.length < this.config.minOperators) return null;
|
|
215
|
+
const last = this.getLastUsedOperator();
|
|
216
|
+
switch (this.config.strategy) {
|
|
217
|
+
case "round-robin":
|
|
218
|
+
return eligible[this.currentIndex % eligible.length];
|
|
219
|
+
case "min-exposure": {
|
|
220
|
+
if (last) {
|
|
221
|
+
const lastUsage = this.usage.get(last.slug);
|
|
222
|
+
const others = eligible.filter((o) => o.slug !== last.slug);
|
|
223
|
+
if (others.length > 0 && lastUsage && lastUsage.requestCount > 0) {
|
|
224
|
+
const sorted2 = others.sort(
|
|
225
|
+
(a, b) => (this.usage.get(a.slug)?.requestCount || 0) - (this.usage.get(b.slug)?.requestCount || 0)
|
|
226
|
+
);
|
|
227
|
+
return sorted2[0];
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
const sorted = [...eligible].sort(
|
|
231
|
+
(a, b) => (this.usage.get(a.slug)?.requestCount || 0) - (this.usage.get(b.slug)?.requestCount || 0)
|
|
232
|
+
);
|
|
233
|
+
return sorted[0];
|
|
234
|
+
}
|
|
235
|
+
case "geo-distributed": {
|
|
236
|
+
const regionUsage = /* @__PURE__ */ new Map();
|
|
237
|
+
for (const op of eligible) {
|
|
238
|
+
const usage = this.usage.get(op.slug)?.requestCount || 0;
|
|
239
|
+
const current = regionUsage.get(op.region) || 0;
|
|
240
|
+
regionUsage.set(op.region, current + usage);
|
|
241
|
+
}
|
|
242
|
+
const sortedRegions = [...regionUsage.entries()].sort((a, b) => a[1] - b[1]);
|
|
243
|
+
const targetRegion = sortedRegions[0]?.[0];
|
|
244
|
+
const regionOps = eligible.filter((o) => o.region === targetRegion);
|
|
245
|
+
return regionOps[0] || eligible[0];
|
|
246
|
+
}
|
|
247
|
+
case "random":
|
|
248
|
+
case "latency-aware":
|
|
249
|
+
default:
|
|
250
|
+
if (last && eligible.length > 1) {
|
|
251
|
+
return eligible.find((o) => o.slug !== last.slug) || eligible[0];
|
|
252
|
+
}
|
|
253
|
+
return eligible[0];
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
|
|
43
258
|
// src/client.ts
|
|
44
259
|
var DEFAULT_BASE_URL = "https://router.tangle.tools/v1";
|
|
45
260
|
async function proxiedFetch(privacy, url, init, streaming) {
|
|
@@ -80,25 +295,44 @@ async function proxiedFetch(privacy, url, init, streaming) {
|
|
|
80
295
|
}
|
|
81
296
|
return fetch(url, init);
|
|
82
297
|
}
|
|
83
|
-
var
|
|
298
|
+
var DEFAULT_RETRY = {
|
|
299
|
+
maxRetries: 3,
|
|
300
|
+
initialBackoffMs: 500,
|
|
301
|
+
maxBackoffMs: 3e4,
|
|
302
|
+
multiplier: 2,
|
|
303
|
+
retryableStatuses: [429, 500, 502, 503, 504]
|
|
304
|
+
};
|
|
305
|
+
var DEFAULT_TIMEOUT_MS = 6e4;
|
|
306
|
+
var DEFAULT_PLATFORM_URL = "https://id.tangle.tools";
|
|
307
|
+
var TCloudClient = class _TCloudClient {
|
|
84
308
|
baseURL;
|
|
309
|
+
platformURL;
|
|
85
310
|
apiKey;
|
|
86
311
|
model;
|
|
87
312
|
headers;
|
|
88
313
|
spendAuthFn;
|
|
89
314
|
privacy;
|
|
90
315
|
limits;
|
|
316
|
+
retryConfig;
|
|
317
|
+
timeoutMs;
|
|
91
318
|
_totalSpent = 0;
|
|
92
319
|
_requestCount = 0;
|
|
320
|
+
privateRouter;
|
|
321
|
+
_cachedOperators = [];
|
|
322
|
+
_operatorsCachedAt = 0;
|
|
323
|
+
static OPERATORS_TTL_MS = 5 * 60 * 1e3;
|
|
93
324
|
constructor(config = {}) {
|
|
94
325
|
this.baseURL = (config.baseURL || DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
95
|
-
this.
|
|
326
|
+
this.platformURL = (config.platformURL || DEFAULT_PLATFORM_URL).replace(/\/$/, "");
|
|
327
|
+
this.apiKey = config.apiKey || process.env.TCLOUD_API_KEY;
|
|
96
328
|
this.model = config.model || "gpt-4o-mini";
|
|
97
329
|
this.privacy = config.privacy;
|
|
98
330
|
this.limits = config.limits;
|
|
331
|
+
this.retryConfig = config.retry === false ? null : { ...DEFAULT_RETRY, ...config.retry };
|
|
332
|
+
this.timeoutMs = config.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
99
333
|
this.headers = {
|
|
100
334
|
"Content-Type": "application/json",
|
|
101
|
-
"X-Tangle-Client": "tcloud-sdk/0.
|
|
335
|
+
"X-Tangle-Client": "tcloud-sdk/0.2.0"
|
|
102
336
|
};
|
|
103
337
|
if (this.apiKey) {
|
|
104
338
|
this.headers["Authorization"] = `Bearer ${this.apiKey}`;
|
|
@@ -118,6 +352,17 @@ var TCloudClient = class {
|
|
|
118
352
|
if (config.routing?.region) {
|
|
119
353
|
this.headers["X-Tangle-Region"] = config.routing.region;
|
|
120
354
|
}
|
|
355
|
+
if (config.routing?.strategy) {
|
|
356
|
+
const strategyMap = {
|
|
357
|
+
"round-robin": "round-robin",
|
|
358
|
+
"lowest-latency": "latency-aware",
|
|
359
|
+
"lowest-price": "round-robin",
|
|
360
|
+
"highest-reputation": "round-robin"
|
|
361
|
+
};
|
|
362
|
+
this.privateRouter = new PrivateRouter({
|
|
363
|
+
strategy: strategyMap[config.routing.strategy] || "round-robin"
|
|
364
|
+
});
|
|
365
|
+
}
|
|
121
366
|
}
|
|
122
367
|
/** Set the SpendAuth signer for private mode */
|
|
123
368
|
setSpendAuthSigner(fn) {
|
|
@@ -151,6 +396,25 @@ var TCloudClient = class {
|
|
|
151
396
|
if (pct >= 0.8) this.limits.onLimitWarning({ type: "total", current: this._totalSpent, limit: this.limits.maxTotalSpend });
|
|
152
397
|
}
|
|
153
398
|
}
|
|
399
|
+
/** Ensure the private router has operators loaded (with TTL-based caching) */
|
|
400
|
+
async ensureRouterOperators() {
|
|
401
|
+
if (!this.privateRouter) return;
|
|
402
|
+
const now = Date.now();
|
|
403
|
+
if (this._cachedOperators.length > 0 && now - this._operatorsCachedAt < _TCloudClient.OPERATORS_TTL_MS) {
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
const data = await this.operators();
|
|
407
|
+
this._cachedOperators = (data.operators || []).map((op) => ({
|
|
408
|
+
slug: op.slug,
|
|
409
|
+
endpointUrl: op.endpointUrl,
|
|
410
|
+
region: "",
|
|
411
|
+
reputationScore: op.reputationScore,
|
|
412
|
+
avgLatencyMs: op.avgLatencyMs,
|
|
413
|
+
models: op.models.map((m) => m.modelId)
|
|
414
|
+
}));
|
|
415
|
+
this._operatorsCachedAt = now;
|
|
416
|
+
this.privateRouter.setOperators(this._cachedOperators);
|
|
417
|
+
}
|
|
154
418
|
/** Track cost after a response, using actual pricing from response headers when available */
|
|
155
419
|
trackCost(completion, res) {
|
|
156
420
|
this._requestCount++;
|
|
@@ -170,36 +434,154 @@ var TCloudClient = class {
|
|
|
170
434
|
}
|
|
171
435
|
}
|
|
172
436
|
}
|
|
173
|
-
/**
|
|
174
|
-
|
|
437
|
+
/**
|
|
438
|
+
* Core fetch with retry + timeout. All helpers build on this.
|
|
439
|
+
* Retries on retryable status codes with exponential backoff + jitter.
|
|
440
|
+
*/
|
|
441
|
+
async _doFetch(url, init, streaming) {
|
|
442
|
+
const retry = this.retryConfig;
|
|
443
|
+
const maxAttempts = retry ? retry.maxRetries + 1 : 1;
|
|
444
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
445
|
+
const controller = new AbortController();
|
|
446
|
+
let timer;
|
|
447
|
+
if (this.timeoutMs > 0 && !streaming) {
|
|
448
|
+
timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
449
|
+
}
|
|
450
|
+
try {
|
|
451
|
+
const res = await proxiedFetch(this.privacy, url, {
|
|
452
|
+
...init,
|
|
453
|
+
signal: controller.signal
|
|
454
|
+
}, streaming);
|
|
455
|
+
if (res.ok) return res;
|
|
456
|
+
if (retry && attempt < retry.maxRetries && retry.retryableStatuses.includes(res.status)) {
|
|
457
|
+
const backoff = Math.min(
|
|
458
|
+
retry.initialBackoffMs * Math.pow(retry.multiplier, attempt),
|
|
459
|
+
retry.maxBackoffMs
|
|
460
|
+
);
|
|
461
|
+
const jitter = backoff * 0.5 * Math.random();
|
|
462
|
+
await new Promise((r) => setTimeout(r, backoff + jitter));
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
466
|
+
throw new TCloudError(res.status, err.error?.message || err.error || err.message || res.statusText);
|
|
467
|
+
} catch (e) {
|
|
468
|
+
if (e instanceof TCloudError) throw e;
|
|
469
|
+
if (retry && attempt < retry.maxRetries) {
|
|
470
|
+
const backoff = Math.min(
|
|
471
|
+
retry.initialBackoffMs * Math.pow(retry.multiplier, attempt),
|
|
472
|
+
retry.maxBackoffMs
|
|
473
|
+
);
|
|
474
|
+
await new Promise((r) => setTimeout(r, backoff));
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
if (e?.name === "AbortError") {
|
|
478
|
+
throw new TCloudError(408, `Request timed out after ${this.timeoutMs}ms`);
|
|
479
|
+
}
|
|
480
|
+
throw new TCloudError(0, e?.message || "Network error");
|
|
481
|
+
} finally {
|
|
482
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
throw new TCloudError(0, "Retry loop exhausted");
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Shared request helper for billable JSON API calls.
|
|
489
|
+
* Enforces: checkLimits → fetch with retry/timeout → error parsing → requestCount.
|
|
490
|
+
*/
|
|
491
|
+
async _request(url, init = {}) {
|
|
175
492
|
this.checkLimits();
|
|
493
|
+
const res = await this._doFetch(url, { headers: this.headers, ...init }, false);
|
|
494
|
+
this._requestCount++;
|
|
495
|
+
return res.json();
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Shared request helper for read-only/non-billable JSON API calls.
|
|
499
|
+
* No limits check, no request counting.
|
|
500
|
+
*/
|
|
501
|
+
async _fetch(url, init = {}) {
|
|
502
|
+
const res = await this._doFetch(url, { headers: this.headers, ...init }, false);
|
|
503
|
+
return res.json();
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Shared request helper for billable calls that return non-JSON (e.g. ArrayBuffer).
|
|
507
|
+
*/
|
|
508
|
+
async _requestRaw(url, init = {}) {
|
|
509
|
+
this.checkLimits();
|
|
510
|
+
const res = await this._doFetch(url, { headers: this.headers, ...init }, false);
|
|
511
|
+
this._requestCount++;
|
|
512
|
+
return res;
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Prepare headers for chat requests — operator routing + SpendAuth +
|
|
516
|
+
* bridge short-circuit headers when `options.bridge` is set.
|
|
517
|
+
* Shared between chat() and chatStream() to eliminate duplication.
|
|
518
|
+
*/
|
|
519
|
+
async _prepareChatRequest(model, bridge) {
|
|
176
520
|
const headers = { ...this.headers };
|
|
177
521
|
if (this.spendAuthFn) {
|
|
178
522
|
const auth = await this.spendAuthFn();
|
|
179
523
|
headers["X-Payment-Signature"] = JSON.stringify(auth);
|
|
180
524
|
delete headers["Authorization"];
|
|
181
525
|
}
|
|
182
|
-
|
|
526
|
+
let baseURL = this.baseURL;
|
|
527
|
+
if (this.privateRouter) {
|
|
528
|
+
await this.ensureRouterOperators();
|
|
529
|
+
const operator = this.privateRouter.selectOperator(model);
|
|
530
|
+
if (operator) {
|
|
531
|
+
baseURL = operator.endpointUrl.replace(/\/$/, "");
|
|
532
|
+
headers["X-Tangle-Operator"] = operator.slug;
|
|
533
|
+
delete headers["Authorization"];
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
if (bridge) {
|
|
537
|
+
headers["X-Bridge-Unlock"] = bridge.unlock;
|
|
538
|
+
if (bridge.resume) headers["X-Resume"] = bridge.resume;
|
|
539
|
+
if (bridge.bridgeUrl) headers["X-Bridge-Url"] = bridge.bridgeUrl;
|
|
540
|
+
if (bridge.bridgeBearer) headers["X-Bridge-Bearer"] = bridge.bridgeBearer;
|
|
541
|
+
}
|
|
542
|
+
return { headers, baseURL };
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Resolve the effective model string. When a bridge is set, rewrite to
|
|
546
|
+
* `bridge/<harness>/<model>` (or `bridge/<harness>` if no model).
|
|
547
|
+
*/
|
|
548
|
+
_effectiveModel(options) {
|
|
549
|
+
if (options.bridge) {
|
|
550
|
+
return options.bridge.model ? `bridge/${options.bridge.harness}/${options.bridge.model}` : `bridge/${options.bridge.harness}`;
|
|
551
|
+
}
|
|
552
|
+
return options.model || this.model;
|
|
553
|
+
}
|
|
554
|
+
/** Build the chat completions request body */
|
|
555
|
+
_chatBody(options, stream) {
|
|
556
|
+
return JSON.stringify({
|
|
557
|
+
model: this._effectiveModel(options),
|
|
558
|
+
messages: options.messages,
|
|
559
|
+
temperature: options.temperature,
|
|
560
|
+
max_tokens: options.maxTokens,
|
|
561
|
+
stream,
|
|
562
|
+
stop: options.stop,
|
|
563
|
+
top_p: options.topP,
|
|
564
|
+
frequency_penalty: options.frequencyPenalty,
|
|
565
|
+
presence_penalty: options.presencePenalty,
|
|
566
|
+
response_format: options.responseFormat,
|
|
567
|
+
tools: options.tools,
|
|
568
|
+
tool_choice: options.toolChoice,
|
|
569
|
+
...options.gateway ? { gateway: options.gateway } : {},
|
|
570
|
+
...options.providerOptions
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
/** Chat completion (non-streaming) */
|
|
574
|
+
async chat(options) {
|
|
575
|
+
this.checkLimits();
|
|
576
|
+
const { headers, baseURL } = await this._prepareChatRequest(
|
|
577
|
+
this._effectiveModel(options),
|
|
578
|
+
options.bridge
|
|
579
|
+
);
|
|
580
|
+
const res = await this._doFetch(`${baseURL}/chat/completions`, {
|
|
183
581
|
method: "POST",
|
|
184
582
|
headers,
|
|
185
|
-
body:
|
|
186
|
-
model: options.model || this.model,
|
|
187
|
-
messages: options.messages,
|
|
188
|
-
temperature: options.temperature,
|
|
189
|
-
max_tokens: options.maxTokens,
|
|
190
|
-
stream: false,
|
|
191
|
-
stop: options.stop,
|
|
192
|
-
top_p: options.topP,
|
|
193
|
-
frequency_penalty: options.frequencyPenalty,
|
|
194
|
-
presence_penalty: options.presencePenalty,
|
|
195
|
-
response_format: options.responseFormat,
|
|
196
|
-
tools: options.tools
|
|
197
|
-
})
|
|
583
|
+
body: this._chatBody(options, false)
|
|
198
584
|
}, false);
|
|
199
|
-
if (!res.ok) {
|
|
200
|
-
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
201
|
-
throw new TCloudError(res.status, err.error?.message || err.error || err.message || res.statusText);
|
|
202
|
-
}
|
|
203
585
|
const completion = await res.json();
|
|
204
586
|
this.trackCost(completion, res);
|
|
205
587
|
return completion;
|
|
@@ -207,29 +589,16 @@ var TCloudClient = class {
|
|
|
207
589
|
/** Chat completion (streaming) — returns an async iterator of chunks */
|
|
208
590
|
async *chatStream(options) {
|
|
209
591
|
this.checkLimits();
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
}
|
|
216
|
-
const res = await proxiedFetch(this.privacy, `${this.baseURL}/chat/completions`, {
|
|
592
|
+
this._requestCount++;
|
|
593
|
+
const { headers, baseURL } = await this._prepareChatRequest(
|
|
594
|
+
this._effectiveModel(options),
|
|
595
|
+
options.bridge
|
|
596
|
+
);
|
|
597
|
+
const res = await this._doFetch(`${baseURL}/chat/completions`, {
|
|
217
598
|
method: "POST",
|
|
218
599
|
headers,
|
|
219
|
-
body:
|
|
220
|
-
model: options.model || this.model,
|
|
221
|
-
messages: options.messages,
|
|
222
|
-
temperature: options.temperature,
|
|
223
|
-
max_tokens: options.maxTokens,
|
|
224
|
-
stream: true,
|
|
225
|
-
stop: options.stop,
|
|
226
|
-
top_p: options.topP
|
|
227
|
-
})
|
|
600
|
+
body: this._chatBody(options, true)
|
|
228
601
|
}, true);
|
|
229
|
-
if (!res.ok) {
|
|
230
|
-
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
231
|
-
throw new TCloudError(res.status, err.error || err.message || res.statusText);
|
|
232
|
-
}
|
|
233
602
|
const reader = res.body.getReader();
|
|
234
603
|
const decoder = new TextDecoder();
|
|
235
604
|
let buf = "";
|
|
@@ -237,13 +606,13 @@ var TCloudClient = class {
|
|
|
237
606
|
const { done, value } = await reader.read();
|
|
238
607
|
if (done) break;
|
|
239
608
|
buf += decoder.decode(value, { stream: true });
|
|
609
|
+
if (buf.length > 1048576) throw new TCloudError(502, "SSE buffer overflow \u2014 server sent >1MB without newline");
|
|
240
610
|
const lines = buf.split("\n");
|
|
241
611
|
buf = lines.pop() || "";
|
|
242
612
|
for (const line of lines) {
|
|
243
613
|
if (!line.startsWith("data: ")) continue;
|
|
244
614
|
const data = line.slice(6).trim();
|
|
245
615
|
if (data === "[DONE]") {
|
|
246
|
-
this._requestCount++;
|
|
247
616
|
return;
|
|
248
617
|
}
|
|
249
618
|
try {
|
|
@@ -253,6 +622,24 @@ var TCloudClient = class {
|
|
|
253
622
|
}
|
|
254
623
|
}
|
|
255
624
|
}
|
|
625
|
+
/**
|
|
626
|
+
* Bridge — scoped helper for a subscription-backed CLI harness behind
|
|
627
|
+
* the Tangle Router's cli-bridge. Returns a mini-client bound to
|
|
628
|
+
* (harness, unlock, resume) so you don't thread those through every
|
|
629
|
+
* call.
|
|
630
|
+
*
|
|
631
|
+
* ```ts
|
|
632
|
+
* const kimi = tcloud.bridge({ harness: 'kimi', model: 'kimi-for-coding', unlock: UNLOCK, resume: 'pr-42' })
|
|
633
|
+
* await kimi.ask('review this diff…')
|
|
634
|
+
* for await (const chunk of kimi.stream('continue…')) process.stdout.write(chunk)
|
|
635
|
+
* ```
|
|
636
|
+
*
|
|
637
|
+
* Sessions persist across process restarts — use the same `resume` id
|
|
638
|
+
* to land on the same CLI conversation (context intact, no replay tax).
|
|
639
|
+
*/
|
|
640
|
+
bridge(cfg) {
|
|
641
|
+
return new BridgeSession(this, cfg);
|
|
642
|
+
}
|
|
256
643
|
/** Convenience: send a single message and get the text response */
|
|
257
644
|
async ask(message, modelOrOptions) {
|
|
258
645
|
const options = typeof modelOrOptions === "string" ? { model: modelOrOptions } : modelOrOptions;
|
|
@@ -283,85 +670,112 @@ var TCloudClient = class {
|
|
|
283
670
|
}
|
|
284
671
|
/** List available models */
|
|
285
672
|
async models() {
|
|
286
|
-
const
|
|
287
|
-
if (!res.ok) throw new TCloudError(res.status, "Failed to fetch models");
|
|
288
|
-
const data = await res.json();
|
|
673
|
+
const data = await this._fetch(`${this.baseURL}/models`);
|
|
289
674
|
return data.data || [];
|
|
290
675
|
}
|
|
291
676
|
/** List active operators */
|
|
292
677
|
async operators() {
|
|
293
678
|
const apiRoot = this.baseURL.replace(/\/v1$/, "");
|
|
294
|
-
|
|
295
|
-
if (!res.ok) throw new TCloudError(res.status, "Failed to fetch operators");
|
|
296
|
-
return res.json();
|
|
679
|
+
return this._fetch(`${apiRoot}/api/operators`);
|
|
297
680
|
}
|
|
681
|
+
// ── Billing (via id.tangle.tools) ──
|
|
298
682
|
/** Get credit balance */
|
|
299
683
|
async credits() {
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
if (!res.ok) throw new TCloudError(res.status, "Failed to fetch credits");
|
|
303
|
-
return res.json();
|
|
684
|
+
const { data } = await this._fetch(`${this.platformURL}/v1/billing/balance`);
|
|
685
|
+
return data;
|
|
304
686
|
}
|
|
305
|
-
/** Add credits */
|
|
687
|
+
/** Add credits via Stripe checkout. Returns the checkout URL. */
|
|
306
688
|
async addCredits(amount) {
|
|
307
|
-
const
|
|
308
|
-
const res = await proxiedFetch(this.privacy, `${apiRoot}/api/billing`, {
|
|
689
|
+
const { data } = await this._fetch(`${this.platformURL}/v1/billing/topup`, {
|
|
309
690
|
method: "POST",
|
|
310
|
-
headers: this.headers,
|
|
311
691
|
body: JSON.stringify({ amount })
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
return res.json();
|
|
692
|
+
});
|
|
693
|
+
return data;
|
|
315
694
|
}
|
|
316
|
-
/**
|
|
317
|
-
async
|
|
318
|
-
const
|
|
319
|
-
|
|
695
|
+
/** Get transaction history */
|
|
696
|
+
async transactions(limit = 50) {
|
|
697
|
+
const { data } = await this._fetch(`${this.platformURL}/v1/billing/transactions?limit=${limit}`);
|
|
698
|
+
return data;
|
|
699
|
+
}
|
|
700
|
+
// ── API Keys (via id.tangle.tools) ──
|
|
701
|
+
/**
|
|
702
|
+
* Create a new API key.
|
|
703
|
+
* When called with an API key (not session), the new key is automatically
|
|
704
|
+
* a child of the calling key — enabling hierarchical key delegation.
|
|
705
|
+
*
|
|
706
|
+
* Pass `parentKeyId` explicitly to create a child of a specific key.
|
|
707
|
+
* Child keys inherit the parent's product scope, allowedModels, and rpmLimit
|
|
708
|
+
* if not specified. Budget cannot exceed the parent's remaining budget.
|
|
709
|
+
*/
|
|
710
|
+
async createKey(opts) {
|
|
711
|
+
const { data } = await this._fetch(`${this.platformURL}/v1/keys`, {
|
|
320
712
|
method: "POST",
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
if (!res.ok) throw new TCloudError(res.status, "Failed to create API key");
|
|
325
|
-
return res.json();
|
|
713
|
+
body: JSON.stringify(opts)
|
|
714
|
+
});
|
|
715
|
+
return data;
|
|
326
716
|
}
|
|
327
|
-
/**
|
|
328
|
-
async
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
717
|
+
/** Get a single API key by ID */
|
|
718
|
+
async getKey(id) {
|
|
719
|
+
const { data } = await this._fetch(`${this.platformURL}/v1/keys/${id}`);
|
|
720
|
+
return data;
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* List API keys.
|
|
724
|
+
* Pass `children: true` to list child keys of the calling API key.
|
|
725
|
+
*/
|
|
726
|
+
async keys(opts) {
|
|
727
|
+
const q = opts?.children ? "?children=true" : "";
|
|
728
|
+
const { data } = await this._fetch(`${this.platformURL}/v1/keys${q}`);
|
|
729
|
+
return data;
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Update an API key's limits.
|
|
733
|
+
* Can adjust budget, allowedModels, rpmLimit, expiresAt, and name.
|
|
734
|
+
*/
|
|
735
|
+
async updateKey(id, updates) {
|
|
736
|
+
const { data } = await this._fetch(`${this.platformURL}/v1/keys/${id}`, {
|
|
737
|
+
method: "PATCH",
|
|
738
|
+
body: JSON.stringify(updates)
|
|
739
|
+
});
|
|
740
|
+
return data;
|
|
333
741
|
}
|
|
334
|
-
/** Revoke an API key */
|
|
742
|
+
/** Revoke an API key. If the key has children, they are also revoked recursively. */
|
|
335
743
|
async revokeKey(id) {
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
}
|
|
341
|
-
|
|
744
|
+
await this._fetch(`${this.platformURL}/v1/keys/${id}`, { method: "DELETE" });
|
|
745
|
+
}
|
|
746
|
+
/** Rotate an API key — creates new key with same config, revokes old */
|
|
747
|
+
async rotateKey(id) {
|
|
748
|
+
const { data } = await this._fetch(`${this.platformURL}/v1/keys/${id}/rotate`, { method: "POST" });
|
|
749
|
+
return data;
|
|
750
|
+
}
|
|
751
|
+
// ── Projects (via id.tangle.tools) ──
|
|
752
|
+
/** Create a project for usage attribution */
|
|
753
|
+
async createProject(name, product) {
|
|
754
|
+
const { data } = await this._fetch(`${this.platformURL}/v1/projects`, {
|
|
755
|
+
method: "POST",
|
|
756
|
+
body: JSON.stringify({ name, product })
|
|
757
|
+
});
|
|
758
|
+
return data;
|
|
759
|
+
}
|
|
760
|
+
/** List projects */
|
|
761
|
+
async projects() {
|
|
762
|
+
const { data } = await this._fetch(`${this.platformURL}/v1/projects`);
|
|
763
|
+
return data;
|
|
342
764
|
}
|
|
343
765
|
/** Generate embeddings */
|
|
344
766
|
async embeddings(options) {
|
|
345
|
-
|
|
767
|
+
return this._request(`${this.baseURL}/embeddings`, {
|
|
346
768
|
method: "POST",
|
|
347
|
-
headers: this.headers,
|
|
348
769
|
body: JSON.stringify({
|
|
349
770
|
model: options.model || "text-embedding-3-small",
|
|
350
771
|
input: options.input
|
|
351
772
|
})
|
|
352
|
-
}
|
|
353
|
-
if (!res.ok) {
|
|
354
|
-
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
355
|
-
throw new TCloudError(res.status, err.error?.message || err.error || res.statusText);
|
|
356
|
-
}
|
|
357
|
-
this._requestCount++;
|
|
358
|
-
return res.json();
|
|
773
|
+
});
|
|
359
774
|
}
|
|
360
775
|
/** Generate images */
|
|
361
776
|
async imageGenerate(options) {
|
|
362
|
-
|
|
777
|
+
return this._request(`${this.baseURL}/images/generations`, {
|
|
363
778
|
method: "POST",
|
|
364
|
-
headers: this.headers,
|
|
365
779
|
body: JSON.stringify({
|
|
366
780
|
model: options.model || "dall-e-3",
|
|
367
781
|
prompt: options.prompt,
|
|
@@ -370,56 +784,36 @@ var TCloudClient = class {
|
|
|
370
784
|
quality: options.quality,
|
|
371
785
|
response_format: options.response_format
|
|
372
786
|
})
|
|
373
|
-
}
|
|
374
|
-
if (!res.ok) {
|
|
375
|
-
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
376
|
-
throw new TCloudError(res.status, err.error?.message || err.error || res.statusText);
|
|
377
|
-
}
|
|
378
|
-
this._requestCount++;
|
|
379
|
-
return res.json();
|
|
787
|
+
});
|
|
380
788
|
}
|
|
381
789
|
/** Rerank documents by relevance to a query */
|
|
382
790
|
async rerank(options) {
|
|
383
|
-
|
|
791
|
+
return this._request(`${this.baseURL}/rerank`, {
|
|
384
792
|
method: "POST",
|
|
385
|
-
headers: this.headers,
|
|
386
793
|
body: JSON.stringify({
|
|
387
794
|
model: options.model || "rerank-english-v3.0",
|
|
388
795
|
query: options.query,
|
|
389
796
|
documents: options.documents,
|
|
390
797
|
top_n: options.top_n
|
|
391
798
|
})
|
|
392
|
-
}
|
|
393
|
-
if (!res.ok) {
|
|
394
|
-
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
395
|
-
throw new TCloudError(res.status, err.error?.message || err.error || res.statusText);
|
|
396
|
-
}
|
|
397
|
-
this._requestCount++;
|
|
398
|
-
return res.json();
|
|
799
|
+
});
|
|
399
800
|
}
|
|
400
801
|
/** Text-to-speech */
|
|
401
802
|
async speech(options) {
|
|
402
|
-
const res = await
|
|
803
|
+
const res = await this._requestRaw(`${this.baseURL}/audio/speech`, {
|
|
403
804
|
method: "POST",
|
|
404
|
-
headers: this.headers,
|
|
405
805
|
body: JSON.stringify({
|
|
406
806
|
model: options.model || "tts-1",
|
|
407
807
|
input: options.input,
|
|
408
808
|
voice: options.voice || "alloy"
|
|
409
809
|
})
|
|
410
|
-
}
|
|
411
|
-
if (!res.ok) {
|
|
412
|
-
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
413
|
-
throw new TCloudError(res.status, err.error?.message || err.error || res.statusText);
|
|
414
|
-
}
|
|
415
|
-
this._requestCount++;
|
|
810
|
+
});
|
|
416
811
|
return res.arrayBuffer();
|
|
417
812
|
}
|
|
418
813
|
/** Legacy completions endpoint */
|
|
419
814
|
async completions(options) {
|
|
420
|
-
|
|
815
|
+
return this._request(`${this.baseURL}/completions`, {
|
|
421
816
|
method: "POST",
|
|
422
|
-
headers: this.headers,
|
|
423
817
|
body: JSON.stringify({
|
|
424
818
|
model: options.model || this.model,
|
|
425
819
|
prompt: options.prompt,
|
|
@@ -428,13 +822,7 @@ var TCloudClient = class {
|
|
|
428
822
|
stop: options.stop,
|
|
429
823
|
top_p: options.topP
|
|
430
824
|
})
|
|
431
|
-
}
|
|
432
|
-
if (!res.ok) {
|
|
433
|
-
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
434
|
-
throw new TCloudError(res.status, err.error?.message || err.error || res.statusText);
|
|
435
|
-
}
|
|
436
|
-
this._requestCount++;
|
|
437
|
-
return res.json();
|
|
825
|
+
});
|
|
438
826
|
}
|
|
439
827
|
/** Audio transcription (speech-to-text) */
|
|
440
828
|
async transcribe(file, options) {
|
|
@@ -445,6 +833,7 @@ var TCloudClient = class {
|
|
|
445
833
|
if (options?.prompt) formData.append("prompt", options.prompt);
|
|
446
834
|
const headers = { ...this.headers };
|
|
447
835
|
delete headers["Content-Type"];
|
|
836
|
+
this.checkLimits();
|
|
448
837
|
const res = await proxiedFetch(this.privacy, `${this.baseURL}/audio/transcriptions`, {
|
|
449
838
|
method: "POST",
|
|
450
839
|
headers,
|
|
@@ -452,75 +841,181 @@ var TCloudClient = class {
|
|
|
452
841
|
}, false);
|
|
453
842
|
if (!res.ok) {
|
|
454
843
|
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
455
|
-
throw new TCloudError(res.status, err.error?.message || err.error || res.statusText);
|
|
844
|
+
throw new TCloudError(res.status, err.error?.message || err.error || err.message || res.statusText);
|
|
456
845
|
}
|
|
457
846
|
this._requestCount++;
|
|
458
847
|
return res.json();
|
|
459
848
|
}
|
|
460
849
|
/** Create a fine-tuning job */
|
|
461
850
|
async fineTuneCreate(options) {
|
|
462
|
-
|
|
851
|
+
return this._request(`${this.baseURL}/fine_tuning/jobs`, {
|
|
463
852
|
method: "POST",
|
|
464
|
-
headers: this.headers,
|
|
465
853
|
body: JSON.stringify(options)
|
|
466
|
-
}
|
|
467
|
-
if (!res.ok) {
|
|
468
|
-
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
469
|
-
throw new TCloudError(res.status, err.error?.message || err.error || res.statusText);
|
|
470
|
-
}
|
|
471
|
-
this._requestCount++;
|
|
472
|
-
return res.json();
|
|
854
|
+
});
|
|
473
855
|
}
|
|
474
856
|
/** List fine-tuning jobs */
|
|
475
857
|
async fineTuneList() {
|
|
476
|
-
|
|
477
|
-
headers: this.headers
|
|
478
|
-
}, false);
|
|
479
|
-
if (!res.ok) throw new TCloudError(res.status, "Failed to fetch fine-tuning jobs");
|
|
480
|
-
return res.json();
|
|
858
|
+
return this._fetch(`${this.baseURL}/fine_tuning/jobs`);
|
|
481
859
|
}
|
|
482
860
|
/** Submit a batch of chat requests */
|
|
483
861
|
async batch(requests) {
|
|
484
|
-
|
|
862
|
+
return this._request(`${this.baseURL}/batch`, {
|
|
485
863
|
method: "POST",
|
|
486
|
-
headers: this.headers,
|
|
487
864
|
body: JSON.stringify({ requests })
|
|
488
|
-
}
|
|
489
|
-
if (!res.ok) {
|
|
490
|
-
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
491
|
-
throw new TCloudError(res.status, err.error?.message || err.error || res.statusText);
|
|
492
|
-
}
|
|
493
|
-
return res.json();
|
|
865
|
+
});
|
|
494
866
|
}
|
|
495
867
|
/** Get batch job status */
|
|
496
868
|
async batchStatus(jobId) {
|
|
497
|
-
|
|
498
|
-
headers: this.headers
|
|
499
|
-
}, false);
|
|
500
|
-
if (!res.ok) throw new TCloudError(res.status, "Failed to fetch batch status");
|
|
501
|
-
return res.json();
|
|
869
|
+
return this._fetch(`${this.baseURL}/batch?id=${jobId}`);
|
|
502
870
|
}
|
|
503
871
|
/** Generate video */
|
|
504
872
|
async videoGenerate(options) {
|
|
505
|
-
|
|
873
|
+
return this._request(`${this.baseURL}/video/generate`, {
|
|
506
874
|
method: "POST",
|
|
507
|
-
headers: this.headers,
|
|
508
875
|
body: JSON.stringify(options)
|
|
509
|
-
}
|
|
510
|
-
if (!res.ok) {
|
|
511
|
-
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
512
|
-
throw new TCloudError(res.status, err.error?.message || err.error || res.statusText);
|
|
513
|
-
}
|
|
514
|
-
this._requestCount++;
|
|
515
|
-
return res.json();
|
|
876
|
+
});
|
|
516
877
|
}
|
|
517
878
|
/** Get video generation status */
|
|
518
879
|
async videoStatus(id) {
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
880
|
+
return this._fetch(`${this.baseURL}/video?id=${id}`);
|
|
881
|
+
}
|
|
882
|
+
/** Generate an avatar video (lip-synced talking head from audio + face image).
|
|
883
|
+
* Returns 202 with a job_id for async polling via avatarJobStatus(). */
|
|
884
|
+
async avatarGenerate(options) {
|
|
885
|
+
return this._request(`${this.baseURL}/avatar/generate`, {
|
|
886
|
+
method: "POST",
|
|
887
|
+
body: JSON.stringify(options)
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
/** Poll an avatar generation job by ID. */
|
|
891
|
+
async avatarJobStatus(jobId) {
|
|
892
|
+
return this._fetch(`${this.baseURL}/avatar/jobs/${jobId}`);
|
|
893
|
+
}
|
|
894
|
+
/** Poll an avatar job until it reaches a terminal state (completed/failed).
|
|
895
|
+
* Returns the final job status. Throws on failure. */
|
|
896
|
+
async pollAvatarJob(jobId, options) {
|
|
897
|
+
const interval = options?.intervalMs ?? 5e3;
|
|
898
|
+
const timeout = options?.timeoutMs ?? 3e5;
|
|
899
|
+
const deadline = Date.now() + timeout;
|
|
900
|
+
while (Date.now() < deadline) {
|
|
901
|
+
const job = await this.avatarJobStatus(jobId);
|
|
902
|
+
if (job.status === "completed") return job;
|
|
903
|
+
if (job.status === "failed") {
|
|
904
|
+
throw new TCloudError(500, job.error || `Avatar job ${jobId} failed`);
|
|
905
|
+
}
|
|
906
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
907
|
+
}
|
|
908
|
+
throw new TCloudError(408, `Avatar job ${jobId} timed out after ${timeout}ms`);
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
911
|
+
* Watch an async job via SSE until it reaches a terminal state.
|
|
912
|
+
* Works with avatar, video, and training blueprint operators.
|
|
913
|
+
*
|
|
914
|
+
* @param jobId - The job ID returned by the creation endpoint
|
|
915
|
+
* @param options - Optional: operatorUrl override, onEvent callback
|
|
916
|
+
* @returns The final JobEvent (completed/failed/cancelled)
|
|
917
|
+
*/
|
|
918
|
+
async watchJob(jobId, options) {
|
|
919
|
+
const base = options?.operatorUrl?.replace(/\/$/, "") || this.baseURL;
|
|
920
|
+
const url = `${base}/v1/jobs/${encodeURIComponent(jobId)}/events`;
|
|
921
|
+
const timeout = options?.timeout ?? 3e5;
|
|
922
|
+
const controller = new AbortController();
|
|
923
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
924
|
+
try {
|
|
925
|
+
const watchHeaders = {
|
|
926
|
+
...this.headers,
|
|
927
|
+
Accept: "text/event-stream"
|
|
928
|
+
};
|
|
929
|
+
if (options?.operatorUrl) {
|
|
930
|
+
delete watchHeaders["Authorization"];
|
|
931
|
+
}
|
|
932
|
+
if (options?.sseToken) {
|
|
933
|
+
watchHeaders["Authorization"] = `Bearer ${options.sseToken}`;
|
|
934
|
+
}
|
|
935
|
+
const res = await proxiedFetch(this.privacy, url, {
|
|
936
|
+
headers: watchHeaders,
|
|
937
|
+
signal: controller.signal
|
|
938
|
+
}, true);
|
|
939
|
+
if (!res.ok) {
|
|
940
|
+
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
941
|
+
throw new TCloudError(res.status, err.error?.message || err.error || res.statusText);
|
|
942
|
+
}
|
|
943
|
+
const reader = res.body.getReader();
|
|
944
|
+
const decoder = new TextDecoder();
|
|
945
|
+
let buf = "";
|
|
946
|
+
const terminalStatuses = /* @__PURE__ */ new Set(["completed", "failed", "cancelled"]);
|
|
947
|
+
while (true) {
|
|
948
|
+
const { done, value } = await reader.read();
|
|
949
|
+
if (done) {
|
|
950
|
+
throw new TCloudError(502, `SSE stream ended without terminal event for job ${jobId}`);
|
|
951
|
+
}
|
|
952
|
+
buf += decoder.decode(value, { stream: true });
|
|
953
|
+
if (buf.length > 1048576) throw new TCloudError(502, "SSE buffer overflow \u2014 server sent >1MB without newline");
|
|
954
|
+
const lines = buf.split("\n");
|
|
955
|
+
buf = lines.pop() || "";
|
|
956
|
+
for (const line of lines) {
|
|
957
|
+
if (!line.startsWith("data: ")) continue;
|
|
958
|
+
const data = line.slice(6).trim();
|
|
959
|
+
if (!data || data === "[DONE]") continue;
|
|
960
|
+
let event;
|
|
961
|
+
try {
|
|
962
|
+
event = JSON.parse(data);
|
|
963
|
+
} catch {
|
|
964
|
+
continue;
|
|
965
|
+
}
|
|
966
|
+
try {
|
|
967
|
+
options?.onEvent?.(event);
|
|
968
|
+
} catch (cbErr) {
|
|
969
|
+
console.error("watchJob onEvent callback error:", cbErr);
|
|
970
|
+
}
|
|
971
|
+
if (terminalStatuses.has(event.status)) {
|
|
972
|
+
return event;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
} catch (err) {
|
|
977
|
+
if (err?.name === "AbortError") {
|
|
978
|
+
throw new TCloudError(408, `Job ${jobId} timed out after ${timeout}ms`);
|
|
979
|
+
}
|
|
980
|
+
throw err;
|
|
981
|
+
} finally {
|
|
982
|
+
clearTimeout(timer);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
// ---------------------------------------------------------------------------
|
|
986
|
+
// Vector Store (requires operator routing — X-Tangle-Service/Blueprint/Operator)
|
|
987
|
+
// ---------------------------------------------------------------------------
|
|
988
|
+
/** Create a vector collection on the operator's vector store */
|
|
989
|
+
async createCollection(options) {
|
|
990
|
+
return this._request(`${this.baseURL}/collections`, {
|
|
991
|
+
method: "POST",
|
|
992
|
+
body: JSON.stringify(options)
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
/** List collections on the operator's vector store */
|
|
996
|
+
async listCollections() {
|
|
997
|
+
return this._fetch(`${this.baseURL}/collections`);
|
|
998
|
+
}
|
|
999
|
+
/** Upsert vectors into a collection */
|
|
1000
|
+
async upsertVectors(collection, vectors) {
|
|
1001
|
+
return this._request(`${this.baseURL}/collections/${encodeURIComponent(collection)}/upsert`, {
|
|
1002
|
+
method: "POST",
|
|
1003
|
+
body: JSON.stringify({ vectors })
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
/** Similarity search in a collection */
|
|
1007
|
+
async queryVectors(collection, options) {
|
|
1008
|
+
return this._request(`${this.baseURL}/collections/${encodeURIComponent(collection)}/query`, {
|
|
1009
|
+
method: "POST",
|
|
1010
|
+
body: JSON.stringify(options)
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
/** RAG query — embed text + search collection in one call */
|
|
1014
|
+
async ragQuery(options) {
|
|
1015
|
+
return this._request(`${this.baseURL}/rag`, {
|
|
1016
|
+
method: "POST",
|
|
1017
|
+
body: JSON.stringify(options)
|
|
1018
|
+
});
|
|
524
1019
|
}
|
|
525
1020
|
/** Search models by name, provider, or capability */
|
|
526
1021
|
async searchModels(query) {
|
|
@@ -539,7 +1034,176 @@ var TCloudClient = class {
|
|
|
539
1034
|
const outputCost = options.outputTokens * parseFloat(model.pricing.completion);
|
|
540
1035
|
return { inputCost, outputCost, total: inputCost + outputCost };
|
|
541
1036
|
}
|
|
1037
|
+
/**
|
|
1038
|
+
* Get a pricing spectrum across resource tiers for a model.
|
|
1039
|
+
*
|
|
1040
|
+
* Uses REAL per-operator pricing from `operator.models[].inputPrice`.
|
|
1041
|
+
* Each tier filters operators by GPU count and TEE capability, then
|
|
1042
|
+
* reports the cheapest and most expensive operator for that config.
|
|
1043
|
+
*
|
|
1044
|
+
* @param options.model - Model ID to price (falls back to client default)
|
|
1045
|
+
* @param options.tiers - Number of tiers (1-7, default 5)
|
|
1046
|
+
*/
|
|
1047
|
+
async pricingSpectrum(options) {
|
|
1048
|
+
const requestedTiers = Math.max(1, Math.min(options.tiers ?? 5, ALL_TIERS.length));
|
|
1049
|
+
const modelId = options.model || this.model;
|
|
1050
|
+
const selected = selectTiers(ALL_TIERS, requestedTiers);
|
|
1051
|
+
const operatorData = await this.operators();
|
|
1052
|
+
const allOperators = operatorData.operators || [];
|
|
1053
|
+
return selected.map((tier) => {
|
|
1054
|
+
const matching = allOperators.filter((op) => {
|
|
1055
|
+
if (tier.gpu > 0 && (op.gpuCount ?? 0) < tier.gpu) return false;
|
|
1056
|
+
if (tier.tee && !op.teeAttested) return false;
|
|
1057
|
+
return true;
|
|
1058
|
+
});
|
|
1059
|
+
const prices = matching.map((op) => op.models.find((m) => m.modelId === modelId)?.inputPrice).filter((p) => p != null && p > 0).sort((a, b) => a - b);
|
|
1060
|
+
const cheapestPrice = prices[0];
|
|
1061
|
+
const priciestPrice = prices.length > 1 ? prices[prices.length - 1] : void 0;
|
|
1062
|
+
return {
|
|
1063
|
+
tier: tier.name,
|
|
1064
|
+
config: tier,
|
|
1065
|
+
cheapestPrice,
|
|
1066
|
+
priciestPrice: priciestPrice !== cheapestPrice ? priciestPrice : void 0,
|
|
1067
|
+
cheapest: cheapestPrice != null ? formatPrice(cheapestPrice) : "no operators for this config",
|
|
1068
|
+
priciest: priciestPrice != null && priciestPrice !== cheapestPrice ? formatPrice(priciestPrice) : void 0,
|
|
1069
|
+
availableOperators: matching.length,
|
|
1070
|
+
operatorsWithModel: prices.length
|
|
1071
|
+
};
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
// ── Eval ──────────────────────────────────────────────────────────────
|
|
1075
|
+
get _apiRoot() {
|
|
1076
|
+
return this.baseURL.replace(/\/v1$/, "");
|
|
1077
|
+
}
|
|
1078
|
+
async eval(opts) {
|
|
1079
|
+
return this._request(`${this._apiRoot}/api/eval`, { method: "POST", body: JSON.stringify(opts) });
|
|
1080
|
+
}
|
|
1081
|
+
async createSuite(opts) {
|
|
1082
|
+
return this._request(`${this._apiRoot}/api/eval/suites`, { method: "POST", body: JSON.stringify(opts) });
|
|
1083
|
+
}
|
|
1084
|
+
async listSuites() {
|
|
1085
|
+
return this._fetch(`${this._apiRoot}/api/eval/suites`);
|
|
1086
|
+
}
|
|
1087
|
+
async runSuite(suiteId, opts) {
|
|
1088
|
+
return this._request(`${this._apiRoot}/api/eval/suites/${suiteId}/runs`, { method: "POST", body: JSON.stringify(opts || {}) });
|
|
1089
|
+
}
|
|
1090
|
+
async listRuns(suiteId) {
|
|
1091
|
+
return this._fetch(`${this._apiRoot}/api/eval/suites/${suiteId}/runs`);
|
|
1092
|
+
}
|
|
1093
|
+
async getRun(runId) {
|
|
1094
|
+
return this._fetch(`${this._apiRoot}/api/eval/runs/${runId}`);
|
|
1095
|
+
}
|
|
1096
|
+
async setBaseline(runId) {
|
|
1097
|
+
await this._request(`${this._apiRoot}/api/eval/runs/${runId}`, { method: "PATCH", body: JSON.stringify({ baseline: true }) });
|
|
1098
|
+
}
|
|
1099
|
+
// ── Sandbox ──────────────────────────────────────────────────────────
|
|
1100
|
+
async sandboxPricing(opts) {
|
|
1101
|
+
const p = new URLSearchParams();
|
|
1102
|
+
if (opts?.cpu) p.set("cpu", String(opts.cpu));
|
|
1103
|
+
if (opts?.ram) p.set("ram", String(opts.ram));
|
|
1104
|
+
if (opts?.disk) p.set("disk", String(opts.disk));
|
|
1105
|
+
return this._fetch(`${this._apiRoot}/api/sandbox/pricing?${p}`);
|
|
1106
|
+
}
|
|
1107
|
+
async sandboxStatus() {
|
|
1108
|
+
return this._fetch(`${this._apiRoot}/api/sandbox/link-key`);
|
|
1109
|
+
}
|
|
1110
|
+
async sandboxProvision() {
|
|
1111
|
+
return this._request(`${this._apiRoot}/api/sandbox/provision`, { method: "POST" });
|
|
1112
|
+
}
|
|
1113
|
+
async sandboxCreate(opts) {
|
|
1114
|
+
return this._request(`${this._apiRoot}/api/sandbox/sessions`, { method: "POST", body: JSON.stringify(opts) });
|
|
1115
|
+
}
|
|
1116
|
+
async sandboxList() {
|
|
1117
|
+
return this._fetch(`${this._apiRoot}/api/sandbox/sessions`);
|
|
1118
|
+
}
|
|
1119
|
+
async sandboxStats(sandboxId) {
|
|
1120
|
+
return this._fetch(`${this._apiRoot}/api/sandbox/stats/${sandboxId}`);
|
|
1121
|
+
}
|
|
1122
|
+
async sandboxDestroy(sessionId) {
|
|
1123
|
+
return this._request(`${this._apiRoot}/api/sandbox/sessions/${sessionId}`, { method: "DELETE" });
|
|
1124
|
+
}
|
|
1125
|
+
// ── User Info ────────────────────────────────────────────────────────
|
|
1126
|
+
async userInfo() {
|
|
1127
|
+
return this._fetch(`${this._apiRoot}/api/auth/userinfo`);
|
|
1128
|
+
}
|
|
542
1129
|
};
|
|
1130
|
+
var ALL_TIERS = [
|
|
1131
|
+
{ name: "cpu-only", cpu: 4, ramGb: 16, gpu: 0, tee: false },
|
|
1132
|
+
{ name: "gpu", cpu: 8, ramGb: 32, gpu: 1, tee: false },
|
|
1133
|
+
{ name: "gpu-tee", cpu: 8, ramGb: 32, gpu: 1, tee: true },
|
|
1134
|
+
{ name: "multi-gpu", cpu: 32, ramGb: 128, gpu: 2, tee: false },
|
|
1135
|
+
{ name: "multi-gpu-tee", cpu: 32, ramGb: 128, gpu: 2, tee: true },
|
|
1136
|
+
{ name: "max-gpu", cpu: 64, ramGb: 256, gpu: 4, tee: false },
|
|
1137
|
+
{ name: "max-gpu-tee", cpu: 64, ramGb: 256, gpu: 4, tee: true }
|
|
1138
|
+
];
|
|
1139
|
+
var BridgeSession = class _BridgeSession {
|
|
1140
|
+
constructor(client, cfg) {
|
|
1141
|
+
this.client = client;
|
|
1142
|
+
this.cfg = cfg;
|
|
1143
|
+
}
|
|
1144
|
+
/** Full chat completion (non-streaming). */
|
|
1145
|
+
async chat(options) {
|
|
1146
|
+
return this.client.chat({ ...options, bridge: this.cfg });
|
|
1147
|
+
}
|
|
1148
|
+
/** Stream OpenAI chat.completion.chunks. */
|
|
1149
|
+
chatStream(options) {
|
|
1150
|
+
return this.client.chatStream({ ...options, bridge: this.cfg });
|
|
1151
|
+
}
|
|
1152
|
+
/** One-shot: send a string, get the assistant text. */
|
|
1153
|
+
async ask(message, extra) {
|
|
1154
|
+
const completion = await this.chat({
|
|
1155
|
+
messages: [{ role: "user", content: message }],
|
|
1156
|
+
...extra
|
|
1157
|
+
});
|
|
1158
|
+
return completion.choices[0]?.message?.content || "";
|
|
1159
|
+
}
|
|
1160
|
+
/** One-shot: send a string, stream text deltas. */
|
|
1161
|
+
async *stream(message, extra) {
|
|
1162
|
+
for await (const chunk of this.chatStream({
|
|
1163
|
+
messages: [{ role: "user", content: message }],
|
|
1164
|
+
...extra
|
|
1165
|
+
})) {
|
|
1166
|
+
const content = chunk.choices?.[0]?.delta?.content;
|
|
1167
|
+
if (content) yield content;
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
/** Turn-based: send full message history, get assistant text. */
|
|
1171
|
+
async turn(messages, extra) {
|
|
1172
|
+
const completion = await this.chat({ messages, ...extra });
|
|
1173
|
+
return completion.choices[0]?.message?.content || "";
|
|
1174
|
+
}
|
|
1175
|
+
/** Clone with a new resume id — same harness, different logical conversation. */
|
|
1176
|
+
withResume(resume) {
|
|
1177
|
+
return new _BridgeSession(this.client, { ...this.cfg, resume });
|
|
1178
|
+
}
|
|
1179
|
+
/** Clone with a different model inside the same harness. */
|
|
1180
|
+
withModel(model) {
|
|
1181
|
+
return new _BridgeSession(this.client, { ...this.cfg, model });
|
|
1182
|
+
}
|
|
1183
|
+
/** The effective model id that will land on the router (`bridge/<harness>/<model>`). */
|
|
1184
|
+
get model() {
|
|
1185
|
+
return this.cfg.model ? `bridge/${this.cfg.harness}/${this.cfg.model}` : `bridge/${this.cfg.harness}`;
|
|
1186
|
+
}
|
|
1187
|
+
/** The resume id currently bound to this session, if any. */
|
|
1188
|
+
get resume() {
|
|
1189
|
+
return this.cfg.resume;
|
|
1190
|
+
}
|
|
1191
|
+
};
|
|
1192
|
+
function selectTiers(all, n) {
|
|
1193
|
+
if (n >= all.length) return [...all];
|
|
1194
|
+
if (n <= 1) return [all[0]];
|
|
1195
|
+
if (n === 2) return [all[0], all[all.length - 1]];
|
|
1196
|
+
const result = [all[0]];
|
|
1197
|
+
const step = (all.length - 1) / (n - 1);
|
|
1198
|
+
for (let i = 1; i < n - 1; i++) {
|
|
1199
|
+
result.push(all[Math.round(i * step)]);
|
|
1200
|
+
}
|
|
1201
|
+
result.push(all[all.length - 1]);
|
|
1202
|
+
return result;
|
|
1203
|
+
}
|
|
1204
|
+
function formatPrice(pricePerToken) {
|
|
1205
|
+
return `$${(pricePerToken * 1e3).toFixed(6)}/1K tokens`;
|
|
1206
|
+
}
|
|
543
1207
|
var TCloudError = class extends Error {
|
|
544
1208
|
constructor(status, message) {
|
|
545
1209
|
super(message);
|
|
@@ -842,6 +1506,8 @@ var TCloud = class _TCloud extends TCloudClient {
|
|
|
842
1506
|
};
|
|
843
1507
|
// Annotate the CommonJS export names for ESM import in node:
|
|
844
1508
|
0 && (module.exports = {
|
|
1509
|
+
BridgeSession,
|
|
1510
|
+
PrivateRouter,
|
|
845
1511
|
TCloud,
|
|
846
1512
|
TCloudClient,
|
|
847
1513
|
TCloudError,
|