@tangle-network/tcloud 0.1.4 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -10
- package/dist/chunk-HL4CXKET.js +976 -0
- package/dist/{chunk-PILYAKCF.js → chunk-STNZT6YR.js} +4 -2
- package/dist/chunk-VD4RNZOC.js +263 -0
- package/dist/cli.cjs +616 -153
- package/dist/cli.js +3 -2
- package/dist/{shielded-BTi_OftW.d.cts → client-CcuHG7_w.d.cts} +275 -60
- package/dist/{shielded-BTi_OftW.d.ts → client-CcuHG7_w.d.ts} +275 -60
- package/dist/index.cjs +618 -153
- package/dist/index.d.cts +4 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +8 -4
- package/dist/instance.cjs +1235 -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 +616 -153
- package/dist/shielded.d.cts +61 -2
- package/dist/shielded.d.ts +61 -2
- package/dist/shielded.js +2 -1
- package/package.json +14 -2
- package/dist/chunk-KVXWEAK3.js +0 -771
package/dist/index.cjs
CHANGED
|
@@ -30,6 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
+
PrivateRouter: () => PrivateRouter,
|
|
33
34
|
TCloud: () => TCloud,
|
|
34
35
|
TCloudClient: () => TCloudClient,
|
|
35
36
|
TCloudError: () => TCloudError,
|
|
@@ -40,6 +41,219 @@ __export(index_exports, {
|
|
|
40
41
|
});
|
|
41
42
|
module.exports = __toCommonJS(index_exports);
|
|
42
43
|
|
|
44
|
+
// src/private-router.ts
|
|
45
|
+
function secureRandom() {
|
|
46
|
+
const arr = new Uint32Array(1);
|
|
47
|
+
crypto.getRandomValues(arr);
|
|
48
|
+
return arr[0] / (4294967295 + 1);
|
|
49
|
+
}
|
|
50
|
+
var PrivateRouter = class {
|
|
51
|
+
config;
|
|
52
|
+
operators = [];
|
|
53
|
+
usage = /* @__PURE__ */ new Map();
|
|
54
|
+
currentIndex = 0;
|
|
55
|
+
totalRequests = 0;
|
|
56
|
+
constructor(config = {}) {
|
|
57
|
+
this.config = {
|
|
58
|
+
strategy: config.strategy || "round-robin",
|
|
59
|
+
maxRequestsPerOperator: config.maxRequestsPerOperator || 5,
|
|
60
|
+
minOperators: config.minOperators || 3,
|
|
61
|
+
preferRegions: config.preferRegions,
|
|
62
|
+
excludeOperators: config.excludeOperators,
|
|
63
|
+
summarizeOnSwitch: config.summarizeOnSwitch ?? false
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** Set the available operator pool */
|
|
67
|
+
setOperators(operators) {
|
|
68
|
+
let filtered = operators.filter(
|
|
69
|
+
(o) => !this.config.excludeOperators?.includes(o.slug)
|
|
70
|
+
);
|
|
71
|
+
if (this.config.preferRegions?.length) {
|
|
72
|
+
filtered.sort((a, b) => {
|
|
73
|
+
const aPreferred = this.config.preferRegions.includes(a.region) ? 0 : 1;
|
|
74
|
+
const bPreferred = this.config.preferRegions.includes(b.region) ? 0 : 1;
|
|
75
|
+
return aPreferred - bPreferred;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
this.operators = filtered;
|
|
79
|
+
}
|
|
80
|
+
/** Select the next operator for a request */
|
|
81
|
+
selectOperator(model) {
|
|
82
|
+
const eligible = this.operators.filter((o) => o.models.includes(model));
|
|
83
|
+
if (eligible.length === 0) return null;
|
|
84
|
+
if (eligible.length < this.config.minOperators) {
|
|
85
|
+
console.warn(
|
|
86
|
+
`[PrivateRouter] Only ${eligible.length} eligible operator(s) for model "${model}", but minOperators requires ${this.config.minOperators}. Refusing to route.`
|
|
87
|
+
);
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
this.totalRequests++;
|
|
91
|
+
switch (this.config.strategy) {
|
|
92
|
+
case "round-robin":
|
|
93
|
+
return this.roundRobin(eligible);
|
|
94
|
+
case "random":
|
|
95
|
+
return this.random(eligible);
|
|
96
|
+
case "geo-distributed":
|
|
97
|
+
return this.geoDistributed(eligible);
|
|
98
|
+
case "min-exposure":
|
|
99
|
+
return this.minExposure(eligible);
|
|
100
|
+
case "latency-aware":
|
|
101
|
+
return this.latencyAware(eligible);
|
|
102
|
+
default:
|
|
103
|
+
return this.roundRobin(eligible);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/** Should we summarize context before this request? (operator is changing) */
|
|
107
|
+
shouldSummarize(model) {
|
|
108
|
+
if (!this.config.summarizeOnSwitch) return false;
|
|
109
|
+
const next = this.peekNextOperator(model);
|
|
110
|
+
const last = this.getLastUsedOperator();
|
|
111
|
+
return next !== null && last !== null && next.slug !== last.slug;
|
|
112
|
+
}
|
|
113
|
+
/** Get privacy stats */
|
|
114
|
+
getStats() {
|
|
115
|
+
return {
|
|
116
|
+
totalRequests: this.totalRequests,
|
|
117
|
+
operatorsUsed: this.usage.size,
|
|
118
|
+
operatorBreakdown: Array.from(this.usage.values()).map((u) => ({
|
|
119
|
+
slug: u.slug,
|
|
120
|
+
requests: u.requestCount,
|
|
121
|
+
lastUsed: u.lastUsedAt
|
|
122
|
+
})),
|
|
123
|
+
strategy: this.config.strategy
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
// ─── Strategies ────────────────────────────────────────────
|
|
127
|
+
roundRobin(eligible) {
|
|
128
|
+
const op = eligible[this.currentIndex % eligible.length];
|
|
129
|
+
this.currentIndex++;
|
|
130
|
+
this.recordUsage(op);
|
|
131
|
+
return op;
|
|
132
|
+
}
|
|
133
|
+
random(eligible) {
|
|
134
|
+
const idx = Math.floor(secureRandom() * eligible.length);
|
|
135
|
+
const op = eligible[idx];
|
|
136
|
+
this.recordUsage(op);
|
|
137
|
+
return op;
|
|
138
|
+
}
|
|
139
|
+
geoDistributed(eligible) {
|
|
140
|
+
const regionUsage = /* @__PURE__ */ new Map();
|
|
141
|
+
for (const op2 of eligible) {
|
|
142
|
+
const usage = this.usage.get(op2.slug)?.requestCount || 0;
|
|
143
|
+
const current = regionUsage.get(op2.region) || 0;
|
|
144
|
+
regionUsage.set(op2.region, current + usage);
|
|
145
|
+
}
|
|
146
|
+
const sortedRegions = [...regionUsage.entries()].sort((a, b) => a[1] - b[1]);
|
|
147
|
+
const targetRegion = sortedRegions[0]?.[0];
|
|
148
|
+
const regionOps = eligible.filter((o) => o.region === targetRegion);
|
|
149
|
+
const op = regionOps[Math.floor(secureRandom() * regionOps.length)] || eligible[0];
|
|
150
|
+
this.recordUsage(op);
|
|
151
|
+
return op;
|
|
152
|
+
}
|
|
153
|
+
minExposure(eligible) {
|
|
154
|
+
const lastUsed = this.getLastUsedOperator();
|
|
155
|
+
if (lastUsed) {
|
|
156
|
+
const lastUsage = this.usage.get(lastUsed.slug);
|
|
157
|
+
const others = eligible.filter((o) => o.slug !== lastUsed.slug);
|
|
158
|
+
if (others.length > 0 && lastUsage && lastUsage.requestCount > 0) {
|
|
159
|
+
const sorted2 = others.sort(
|
|
160
|
+
(a, b) => (this.usage.get(a.slug)?.requestCount || 0) - (this.usage.get(b.slug)?.requestCount || 0)
|
|
161
|
+
);
|
|
162
|
+
const op2 = sorted2[0];
|
|
163
|
+
this.recordUsage(op2);
|
|
164
|
+
return op2;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const sorted = [...eligible].sort(
|
|
168
|
+
(a, b) => (this.usage.get(a.slug)?.requestCount || 0) - (this.usage.get(b.slug)?.requestCount || 0)
|
|
169
|
+
);
|
|
170
|
+
const op = sorted[0];
|
|
171
|
+
this.recordUsage(op);
|
|
172
|
+
return op;
|
|
173
|
+
}
|
|
174
|
+
latencyAware(eligible) {
|
|
175
|
+
const weights = eligible.map((o) => {
|
|
176
|
+
const latencyWeight = 1 / Math.max(o.avgLatencyMs, 10);
|
|
177
|
+
const usagePenalty = (this.usage.get(o.slug)?.requestCount || 0) * 0.1;
|
|
178
|
+
return Math.max(latencyWeight - usagePenalty, 0.01);
|
|
179
|
+
});
|
|
180
|
+
const totalWeight = weights.reduce((s, w) => s + w, 0);
|
|
181
|
+
let r = secureRandom() * totalWeight;
|
|
182
|
+
for (let i = 0; i < eligible.length; i++) {
|
|
183
|
+
r -= weights[i];
|
|
184
|
+
if (r <= 0) {
|
|
185
|
+
this.recordUsage(eligible[i]);
|
|
186
|
+
return eligible[i];
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const op = eligible[eligible.length - 1];
|
|
190
|
+
this.recordUsage(op);
|
|
191
|
+
return op;
|
|
192
|
+
}
|
|
193
|
+
// ─── Helpers ───────────────────────────────────────────────
|
|
194
|
+
recordUsage(op) {
|
|
195
|
+
const existing = this.usage.get(op.slug);
|
|
196
|
+
this.usage.set(op.slug, {
|
|
197
|
+
slug: op.slug,
|
|
198
|
+
requestCount: (existing?.requestCount || 0) + 1,
|
|
199
|
+
lastUsedAt: Date.now()
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
getLastUsedOperator() {
|
|
203
|
+
let latest = null;
|
|
204
|
+
for (const u of this.usage.values()) {
|
|
205
|
+
if (!latest || u.lastUsedAt > latest.lastUsedAt) latest = u;
|
|
206
|
+
}
|
|
207
|
+
if (!latest) return null;
|
|
208
|
+
return this.operators.find((o) => o.slug === latest.slug) || null;
|
|
209
|
+
}
|
|
210
|
+
peekNextOperator(model) {
|
|
211
|
+
const eligible = this.operators.filter((o) => o.models.includes(model));
|
|
212
|
+
if (eligible.length === 0) return null;
|
|
213
|
+
if (eligible.length < this.config.minOperators) return null;
|
|
214
|
+
const last = this.getLastUsedOperator();
|
|
215
|
+
switch (this.config.strategy) {
|
|
216
|
+
case "round-robin":
|
|
217
|
+
return eligible[this.currentIndex % eligible.length];
|
|
218
|
+
case "min-exposure": {
|
|
219
|
+
if (last) {
|
|
220
|
+
const lastUsage = this.usage.get(last.slug);
|
|
221
|
+
const others = eligible.filter((o) => o.slug !== last.slug);
|
|
222
|
+
if (others.length > 0 && lastUsage && lastUsage.requestCount > 0) {
|
|
223
|
+
const sorted2 = others.sort(
|
|
224
|
+
(a, b) => (this.usage.get(a.slug)?.requestCount || 0) - (this.usage.get(b.slug)?.requestCount || 0)
|
|
225
|
+
);
|
|
226
|
+
return sorted2[0];
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const sorted = [...eligible].sort(
|
|
230
|
+
(a, b) => (this.usage.get(a.slug)?.requestCount || 0) - (this.usage.get(b.slug)?.requestCount || 0)
|
|
231
|
+
);
|
|
232
|
+
return sorted[0];
|
|
233
|
+
}
|
|
234
|
+
case "geo-distributed": {
|
|
235
|
+
const regionUsage = /* @__PURE__ */ new Map();
|
|
236
|
+
for (const op of eligible) {
|
|
237
|
+
const usage = this.usage.get(op.slug)?.requestCount || 0;
|
|
238
|
+
const current = regionUsage.get(op.region) || 0;
|
|
239
|
+
regionUsage.set(op.region, current + usage);
|
|
240
|
+
}
|
|
241
|
+
const sortedRegions = [...regionUsage.entries()].sort((a, b) => a[1] - b[1]);
|
|
242
|
+
const targetRegion = sortedRegions[0]?.[0];
|
|
243
|
+
const regionOps = eligible.filter((o) => o.region === targetRegion);
|
|
244
|
+
return regionOps[0] || eligible[0];
|
|
245
|
+
}
|
|
246
|
+
case "random":
|
|
247
|
+
case "latency-aware":
|
|
248
|
+
default:
|
|
249
|
+
if (last && eligible.length > 1) {
|
|
250
|
+
return eligible.find((o) => o.slug !== last.slug) || eligible[0];
|
|
251
|
+
}
|
|
252
|
+
return eligible[0];
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
|
|
43
257
|
// src/client.ts
|
|
44
258
|
var DEFAULT_BASE_URL = "https://router.tangle.tools/v1";
|
|
45
259
|
async function proxiedFetch(privacy, url, init, streaming) {
|
|
@@ -80,7 +294,15 @@ async function proxiedFetch(privacy, url, init, streaming) {
|
|
|
80
294
|
}
|
|
81
295
|
return fetch(url, init);
|
|
82
296
|
}
|
|
83
|
-
var
|
|
297
|
+
var DEFAULT_RETRY = {
|
|
298
|
+
maxRetries: 3,
|
|
299
|
+
initialBackoffMs: 500,
|
|
300
|
+
maxBackoffMs: 3e4,
|
|
301
|
+
multiplier: 2,
|
|
302
|
+
retryableStatuses: [429, 500, 502, 503, 504]
|
|
303
|
+
};
|
|
304
|
+
var DEFAULT_TIMEOUT_MS = 6e4;
|
|
305
|
+
var TCloudClient = class _TCloudClient {
|
|
84
306
|
baseURL;
|
|
85
307
|
apiKey;
|
|
86
308
|
model;
|
|
@@ -88,17 +310,25 @@ var TCloudClient = class {
|
|
|
88
310
|
spendAuthFn;
|
|
89
311
|
privacy;
|
|
90
312
|
limits;
|
|
313
|
+
retryConfig;
|
|
314
|
+
timeoutMs;
|
|
91
315
|
_totalSpent = 0;
|
|
92
316
|
_requestCount = 0;
|
|
317
|
+
privateRouter;
|
|
318
|
+
_cachedOperators = [];
|
|
319
|
+
_operatorsCachedAt = 0;
|
|
320
|
+
static OPERATORS_TTL_MS = 5 * 60 * 1e3;
|
|
93
321
|
constructor(config = {}) {
|
|
94
322
|
this.baseURL = (config.baseURL || DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
95
|
-
this.apiKey = config.apiKey || process.env.TCLOUD_API_KEY
|
|
323
|
+
this.apiKey = config.apiKey || process.env.TCLOUD_API_KEY;
|
|
96
324
|
this.model = config.model || "gpt-4o-mini";
|
|
97
325
|
this.privacy = config.privacy;
|
|
98
326
|
this.limits = config.limits;
|
|
327
|
+
this.retryConfig = config.retry === false ? null : { ...DEFAULT_RETRY, ...config.retry };
|
|
328
|
+
this.timeoutMs = config.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
99
329
|
this.headers = {
|
|
100
330
|
"Content-Type": "application/json",
|
|
101
|
-
"X-Tangle-Client": "tcloud-sdk/0.
|
|
331
|
+
"X-Tangle-Client": "tcloud-sdk/0.2.0"
|
|
102
332
|
};
|
|
103
333
|
if (this.apiKey) {
|
|
104
334
|
this.headers["Authorization"] = `Bearer ${this.apiKey}`;
|
|
@@ -118,6 +348,17 @@ var TCloudClient = class {
|
|
|
118
348
|
if (config.routing?.region) {
|
|
119
349
|
this.headers["X-Tangle-Region"] = config.routing.region;
|
|
120
350
|
}
|
|
351
|
+
if (config.routing?.strategy) {
|
|
352
|
+
const strategyMap = {
|
|
353
|
+
"round-robin": "round-robin",
|
|
354
|
+
"lowest-latency": "latency-aware",
|
|
355
|
+
"lowest-price": "round-robin",
|
|
356
|
+
"highest-reputation": "round-robin"
|
|
357
|
+
};
|
|
358
|
+
this.privateRouter = new PrivateRouter({
|
|
359
|
+
strategy: strategyMap[config.routing.strategy] || "round-robin"
|
|
360
|
+
});
|
|
361
|
+
}
|
|
121
362
|
}
|
|
122
363
|
/** Set the SpendAuth signer for private mode */
|
|
123
364
|
setSpendAuthSigner(fn) {
|
|
@@ -151,6 +392,25 @@ var TCloudClient = class {
|
|
|
151
392
|
if (pct >= 0.8) this.limits.onLimitWarning({ type: "total", current: this._totalSpent, limit: this.limits.maxTotalSpend });
|
|
152
393
|
}
|
|
153
394
|
}
|
|
395
|
+
/** Ensure the private router has operators loaded (with TTL-based caching) */
|
|
396
|
+
async ensureRouterOperators() {
|
|
397
|
+
if (!this.privateRouter) return;
|
|
398
|
+
const now = Date.now();
|
|
399
|
+
if (this._cachedOperators.length > 0 && now - this._operatorsCachedAt < _TCloudClient.OPERATORS_TTL_MS) {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
const data = await this.operators();
|
|
403
|
+
this._cachedOperators = (data.operators || []).map((op) => ({
|
|
404
|
+
slug: op.slug,
|
|
405
|
+
endpointUrl: op.endpointUrl,
|
|
406
|
+
region: "",
|
|
407
|
+
reputationScore: op.reputationScore,
|
|
408
|
+
avgLatencyMs: op.avgLatencyMs,
|
|
409
|
+
models: op.models.map((m) => m.modelId)
|
|
410
|
+
}));
|
|
411
|
+
this._operatorsCachedAt = now;
|
|
412
|
+
this.privateRouter.setOperators(this._cachedOperators);
|
|
413
|
+
}
|
|
154
414
|
/** Track cost after a response, using actual pricing from response headers when available */
|
|
155
415
|
trackCost(completion, res) {
|
|
156
416
|
this._requestCount++;
|
|
@@ -170,36 +430,133 @@ var TCloudClient = class {
|
|
|
170
430
|
}
|
|
171
431
|
}
|
|
172
432
|
}
|
|
173
|
-
/**
|
|
174
|
-
|
|
433
|
+
/**
|
|
434
|
+
* Core fetch with retry + timeout. All helpers build on this.
|
|
435
|
+
* Retries on retryable status codes with exponential backoff + jitter.
|
|
436
|
+
*/
|
|
437
|
+
async _doFetch(url, init, streaming) {
|
|
438
|
+
const retry = this.retryConfig;
|
|
439
|
+
const maxAttempts = retry ? retry.maxRetries + 1 : 1;
|
|
440
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
441
|
+
const controller = new AbortController();
|
|
442
|
+
let timer;
|
|
443
|
+
if (this.timeoutMs > 0 && !streaming) {
|
|
444
|
+
timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
445
|
+
}
|
|
446
|
+
try {
|
|
447
|
+
const res = await proxiedFetch(this.privacy, url, {
|
|
448
|
+
...init,
|
|
449
|
+
signal: controller.signal
|
|
450
|
+
}, streaming);
|
|
451
|
+
if (res.ok) return res;
|
|
452
|
+
if (retry && attempt < retry.maxRetries && retry.retryableStatuses.includes(res.status)) {
|
|
453
|
+
const backoff = Math.min(
|
|
454
|
+
retry.initialBackoffMs * Math.pow(retry.multiplier, attempt),
|
|
455
|
+
retry.maxBackoffMs
|
|
456
|
+
);
|
|
457
|
+
const jitter = backoff * 0.5 * Math.random();
|
|
458
|
+
await new Promise((r) => setTimeout(r, backoff + jitter));
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
462
|
+
throw new TCloudError(res.status, err.error?.message || err.error || err.message || res.statusText);
|
|
463
|
+
} catch (e) {
|
|
464
|
+
if (e instanceof TCloudError) throw e;
|
|
465
|
+
if (retry && attempt < retry.maxRetries) {
|
|
466
|
+
const backoff = Math.min(
|
|
467
|
+
retry.initialBackoffMs * Math.pow(retry.multiplier, attempt),
|
|
468
|
+
retry.maxBackoffMs
|
|
469
|
+
);
|
|
470
|
+
await new Promise((r) => setTimeout(r, backoff));
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
if (e?.name === "AbortError") {
|
|
474
|
+
throw new TCloudError(408, `Request timed out after ${this.timeoutMs}ms`);
|
|
475
|
+
}
|
|
476
|
+
throw new TCloudError(0, e?.message || "Network error");
|
|
477
|
+
} finally {
|
|
478
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
throw new TCloudError(0, "Retry loop exhausted");
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Shared request helper for billable JSON API calls.
|
|
485
|
+
* Enforces: checkLimits → fetch with retry/timeout → error parsing → requestCount.
|
|
486
|
+
*/
|
|
487
|
+
async _request(url, init = {}) {
|
|
488
|
+
this.checkLimits();
|
|
489
|
+
const res = await this._doFetch(url, { headers: this.headers, ...init }, false);
|
|
490
|
+
this._requestCount++;
|
|
491
|
+
return res.json();
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Shared request helper for read-only/non-billable JSON API calls.
|
|
495
|
+
* No limits check, no request counting.
|
|
496
|
+
*/
|
|
497
|
+
async _fetch(url, init = {}) {
|
|
498
|
+
const res = await this._doFetch(url, { headers: this.headers, ...init }, false);
|
|
499
|
+
return res.json();
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Shared request helper for billable calls that return non-JSON (e.g. ArrayBuffer).
|
|
503
|
+
*/
|
|
504
|
+
async _requestRaw(url, init = {}) {
|
|
175
505
|
this.checkLimits();
|
|
506
|
+
const res = await this._doFetch(url, { headers: this.headers, ...init }, false);
|
|
507
|
+
this._requestCount++;
|
|
508
|
+
return res;
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Prepare headers for chat requests — operator routing + SpendAuth.
|
|
512
|
+
* Shared between chat() and chatStream() to eliminate duplication.
|
|
513
|
+
*/
|
|
514
|
+
async _prepareChatRequest(model) {
|
|
176
515
|
const headers = { ...this.headers };
|
|
177
516
|
if (this.spendAuthFn) {
|
|
178
517
|
const auth = await this.spendAuthFn();
|
|
179
518
|
headers["X-Payment-Signature"] = JSON.stringify(auth);
|
|
180
519
|
delete headers["Authorization"];
|
|
181
520
|
}
|
|
182
|
-
|
|
521
|
+
let baseURL = this.baseURL;
|
|
522
|
+
if (this.privateRouter) {
|
|
523
|
+
await this.ensureRouterOperators();
|
|
524
|
+
const operator = this.privateRouter.selectOperator(model);
|
|
525
|
+
if (operator) {
|
|
526
|
+
baseURL = operator.endpointUrl.replace(/\/$/, "");
|
|
527
|
+
headers["X-Tangle-Operator"] = operator.slug;
|
|
528
|
+
delete headers["Authorization"];
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
return { headers, baseURL };
|
|
532
|
+
}
|
|
533
|
+
/** Build the chat completions request body */
|
|
534
|
+
_chatBody(options, stream) {
|
|
535
|
+
return JSON.stringify({
|
|
536
|
+
model: options.model || this.model,
|
|
537
|
+
messages: options.messages,
|
|
538
|
+
temperature: options.temperature,
|
|
539
|
+
max_tokens: options.maxTokens,
|
|
540
|
+
stream,
|
|
541
|
+
stop: options.stop,
|
|
542
|
+
top_p: options.topP,
|
|
543
|
+
frequency_penalty: options.frequencyPenalty,
|
|
544
|
+
presence_penalty: options.presencePenalty,
|
|
545
|
+
response_format: options.responseFormat,
|
|
546
|
+
tools: options.tools,
|
|
547
|
+
tool_choice: options.toolChoice,
|
|
548
|
+
...options.providerOptions
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
/** Chat completion (non-streaming) */
|
|
552
|
+
async chat(options) {
|
|
553
|
+
this.checkLimits();
|
|
554
|
+
const { headers, baseURL } = await this._prepareChatRequest(options.model || this.model);
|
|
555
|
+
const res = await this._doFetch(`${baseURL}/chat/completions`, {
|
|
183
556
|
method: "POST",
|
|
184
557
|
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
|
-
})
|
|
558
|
+
body: this._chatBody(options, false)
|
|
198
559
|
}, 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
560
|
const completion = await res.json();
|
|
204
561
|
this.trackCost(completion, res);
|
|
205
562
|
return completion;
|
|
@@ -207,29 +564,13 @@ var TCloudClient = class {
|
|
|
207
564
|
/** Chat completion (streaming) — returns an async iterator of chunks */
|
|
208
565
|
async *chatStream(options) {
|
|
209
566
|
this.checkLimits();
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
headers["X-Payment-Signature"] = JSON.stringify(auth);
|
|
214
|
-
delete headers["Authorization"];
|
|
215
|
-
}
|
|
216
|
-
const res = await proxiedFetch(this.privacy, `${this.baseURL}/chat/completions`, {
|
|
567
|
+
this._requestCount++;
|
|
568
|
+
const { headers, baseURL } = await this._prepareChatRequest(options.model || this.model);
|
|
569
|
+
const res = await this._doFetch(`${baseURL}/chat/completions`, {
|
|
217
570
|
method: "POST",
|
|
218
571
|
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
|
-
})
|
|
572
|
+
body: this._chatBody(options, true)
|
|
228
573
|
}, 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
574
|
const reader = res.body.getReader();
|
|
234
575
|
const decoder = new TextDecoder();
|
|
235
576
|
let buf = "";
|
|
@@ -237,13 +578,13 @@ var TCloudClient = class {
|
|
|
237
578
|
const { done, value } = await reader.read();
|
|
238
579
|
if (done) break;
|
|
239
580
|
buf += decoder.decode(value, { stream: true });
|
|
581
|
+
if (buf.length > 1048576) throw new TCloudError(502, "SSE buffer overflow \u2014 server sent >1MB without newline");
|
|
240
582
|
const lines = buf.split("\n");
|
|
241
583
|
buf = lines.pop() || "";
|
|
242
584
|
for (const line of lines) {
|
|
243
585
|
if (!line.startsWith("data: ")) continue;
|
|
244
586
|
const data = line.slice(6).trim();
|
|
245
587
|
if (data === "[DONE]") {
|
|
246
|
-
this._requestCount++;
|
|
247
588
|
return;
|
|
248
589
|
}
|
|
249
590
|
try {
|
|
@@ -283,53 +624,39 @@ var TCloudClient = class {
|
|
|
283
624
|
}
|
|
284
625
|
/** List available models */
|
|
285
626
|
async models() {
|
|
286
|
-
const
|
|
287
|
-
if (!res.ok) throw new TCloudError(res.status, "Failed to fetch models");
|
|
288
|
-
const data = await res.json();
|
|
627
|
+
const data = await this._fetch(`${this.baseURL}/models`);
|
|
289
628
|
return data.data || [];
|
|
290
629
|
}
|
|
291
630
|
/** List active operators */
|
|
292
631
|
async operators() {
|
|
293
632
|
const apiRoot = this.baseURL.replace(/\/v1$/, "");
|
|
294
|
-
|
|
295
|
-
if (!res.ok) throw new TCloudError(res.status, "Failed to fetch operators");
|
|
296
|
-
return res.json();
|
|
633
|
+
return this._fetch(`${apiRoot}/api/operators`);
|
|
297
634
|
}
|
|
298
635
|
/** Get credit balance */
|
|
299
636
|
async credits() {
|
|
300
637
|
const apiRoot = this.baseURL.replace(/\/v1$/, "");
|
|
301
|
-
|
|
302
|
-
if (!res.ok) throw new TCloudError(res.status, "Failed to fetch credits");
|
|
303
|
-
return res.json();
|
|
638
|
+
return this._fetch(`${apiRoot}/api/billing`);
|
|
304
639
|
}
|
|
305
640
|
/** Add credits */
|
|
306
641
|
async addCredits(amount) {
|
|
307
642
|
const apiRoot = this.baseURL.replace(/\/v1$/, "");
|
|
308
|
-
|
|
643
|
+
return this._fetch(`${apiRoot}/api/billing`, {
|
|
309
644
|
method: "POST",
|
|
310
|
-
headers: this.headers,
|
|
311
645
|
body: JSON.stringify({ amount })
|
|
312
|
-
}
|
|
313
|
-
if (!res.ok) throw new TCloudError(res.status, "Failed to add credits");
|
|
314
|
-
return res.json();
|
|
646
|
+
});
|
|
315
647
|
}
|
|
316
648
|
/** Create a new API key */
|
|
317
649
|
async createKey(name) {
|
|
318
650
|
const apiRoot = this.baseURL.replace(/\/v1$/, "");
|
|
319
|
-
|
|
651
|
+
return this._fetch(`${apiRoot}/api/keys`, {
|
|
320
652
|
method: "POST",
|
|
321
|
-
headers: this.headers,
|
|
322
653
|
body: JSON.stringify({ name })
|
|
323
|
-
}
|
|
324
|
-
if (!res.ok) throw new TCloudError(res.status, "Failed to create API key");
|
|
325
|
-
return res.json();
|
|
654
|
+
});
|
|
326
655
|
}
|
|
327
656
|
/** List API keys */
|
|
328
657
|
async keys() {
|
|
329
658
|
const apiRoot = this.baseURL.replace(/\/v1$/, "");
|
|
330
|
-
|
|
331
|
-
if (!res.ok) throw new TCloudError(res.status, "Failed to fetch keys");
|
|
332
|
-
return res.json();
|
|
659
|
+
return this._fetch(`${apiRoot}/api/keys`);
|
|
333
660
|
}
|
|
334
661
|
/** Revoke an API key */
|
|
335
662
|
async revokeKey(id) {
|
|
@@ -338,30 +665,25 @@ var TCloudClient = class {
|
|
|
338
665
|
method: "DELETE",
|
|
339
666
|
headers: this.headers
|
|
340
667
|
}, false);
|
|
341
|
-
if (!res.ok)
|
|
668
|
+
if (!res.ok) {
|
|
669
|
+
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
670
|
+
throw new TCloudError(res.status, err.error?.message || err.error || err.message || res.statusText);
|
|
671
|
+
}
|
|
342
672
|
}
|
|
343
673
|
/** Generate embeddings */
|
|
344
674
|
async embeddings(options) {
|
|
345
|
-
|
|
675
|
+
return this._request(`${this.baseURL}/embeddings`, {
|
|
346
676
|
method: "POST",
|
|
347
|
-
headers: this.headers,
|
|
348
677
|
body: JSON.stringify({
|
|
349
678
|
model: options.model || "text-embedding-3-small",
|
|
350
679
|
input: options.input
|
|
351
680
|
})
|
|
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();
|
|
681
|
+
});
|
|
359
682
|
}
|
|
360
683
|
/** Generate images */
|
|
361
684
|
async imageGenerate(options) {
|
|
362
|
-
|
|
685
|
+
return this._request(`${this.baseURL}/images/generations`, {
|
|
363
686
|
method: "POST",
|
|
364
|
-
headers: this.headers,
|
|
365
687
|
body: JSON.stringify({
|
|
366
688
|
model: options.model || "dall-e-3",
|
|
367
689
|
prompt: options.prompt,
|
|
@@ -370,56 +692,36 @@ var TCloudClient = class {
|
|
|
370
692
|
quality: options.quality,
|
|
371
693
|
response_format: options.response_format
|
|
372
694
|
})
|
|
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();
|
|
695
|
+
});
|
|
380
696
|
}
|
|
381
697
|
/** Rerank documents by relevance to a query */
|
|
382
698
|
async rerank(options) {
|
|
383
|
-
|
|
699
|
+
return this._request(`${this.baseURL}/rerank`, {
|
|
384
700
|
method: "POST",
|
|
385
|
-
headers: this.headers,
|
|
386
701
|
body: JSON.stringify({
|
|
387
702
|
model: options.model || "rerank-english-v3.0",
|
|
388
703
|
query: options.query,
|
|
389
704
|
documents: options.documents,
|
|
390
705
|
top_n: options.top_n
|
|
391
706
|
})
|
|
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();
|
|
707
|
+
});
|
|
399
708
|
}
|
|
400
709
|
/** Text-to-speech */
|
|
401
710
|
async speech(options) {
|
|
402
|
-
const res = await
|
|
711
|
+
const res = await this._requestRaw(`${this.baseURL}/audio/speech`, {
|
|
403
712
|
method: "POST",
|
|
404
|
-
headers: this.headers,
|
|
405
713
|
body: JSON.stringify({
|
|
406
714
|
model: options.model || "tts-1",
|
|
407
715
|
input: options.input,
|
|
408
716
|
voice: options.voice || "alloy"
|
|
409
717
|
})
|
|
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++;
|
|
718
|
+
});
|
|
416
719
|
return res.arrayBuffer();
|
|
417
720
|
}
|
|
418
721
|
/** Legacy completions endpoint */
|
|
419
722
|
async completions(options) {
|
|
420
|
-
|
|
723
|
+
return this._request(`${this.baseURL}/completions`, {
|
|
421
724
|
method: "POST",
|
|
422
|
-
headers: this.headers,
|
|
423
725
|
body: JSON.stringify({
|
|
424
726
|
model: options.model || this.model,
|
|
425
727
|
prompt: options.prompt,
|
|
@@ -428,13 +730,7 @@ var TCloudClient = class {
|
|
|
428
730
|
stop: options.stop,
|
|
429
731
|
top_p: options.topP
|
|
430
732
|
})
|
|
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();
|
|
733
|
+
});
|
|
438
734
|
}
|
|
439
735
|
/** Audio transcription (speech-to-text) */
|
|
440
736
|
async transcribe(file, options) {
|
|
@@ -445,6 +741,7 @@ var TCloudClient = class {
|
|
|
445
741
|
if (options?.prompt) formData.append("prompt", options.prompt);
|
|
446
742
|
const headers = { ...this.headers };
|
|
447
743
|
delete headers["Content-Type"];
|
|
744
|
+
this.checkLimits();
|
|
448
745
|
const res = await proxiedFetch(this.privacy, `${this.baseURL}/audio/transcriptions`, {
|
|
449
746
|
method: "POST",
|
|
450
747
|
headers,
|
|
@@ -452,75 +749,181 @@ var TCloudClient = class {
|
|
|
452
749
|
}, false);
|
|
453
750
|
if (!res.ok) {
|
|
454
751
|
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
455
|
-
throw new TCloudError(res.status, err.error?.message || err.error || res.statusText);
|
|
752
|
+
throw new TCloudError(res.status, err.error?.message || err.error || err.message || res.statusText);
|
|
456
753
|
}
|
|
457
754
|
this._requestCount++;
|
|
458
755
|
return res.json();
|
|
459
756
|
}
|
|
460
757
|
/** Create a fine-tuning job */
|
|
461
758
|
async fineTuneCreate(options) {
|
|
462
|
-
|
|
759
|
+
return this._request(`${this.baseURL}/fine_tuning/jobs`, {
|
|
463
760
|
method: "POST",
|
|
464
|
-
headers: this.headers,
|
|
465
761
|
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();
|
|
762
|
+
});
|
|
473
763
|
}
|
|
474
764
|
/** List fine-tuning jobs */
|
|
475
765
|
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();
|
|
766
|
+
return this._fetch(`${this.baseURL}/fine_tuning/jobs`);
|
|
481
767
|
}
|
|
482
768
|
/** Submit a batch of chat requests */
|
|
483
769
|
async batch(requests) {
|
|
484
|
-
|
|
770
|
+
return this._request(`${this.baseURL}/batch`, {
|
|
485
771
|
method: "POST",
|
|
486
|
-
headers: this.headers,
|
|
487
772
|
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();
|
|
773
|
+
});
|
|
494
774
|
}
|
|
495
775
|
/** Get batch job status */
|
|
496
776
|
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();
|
|
777
|
+
return this._fetch(`${this.baseURL}/batch?id=${jobId}`);
|
|
502
778
|
}
|
|
503
779
|
/** Generate video */
|
|
504
780
|
async videoGenerate(options) {
|
|
505
|
-
|
|
781
|
+
return this._request(`${this.baseURL}/video/generate`, {
|
|
506
782
|
method: "POST",
|
|
507
|
-
headers: this.headers,
|
|
508
783
|
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();
|
|
784
|
+
});
|
|
516
785
|
}
|
|
517
786
|
/** Get video generation status */
|
|
518
787
|
async videoStatus(id) {
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
788
|
+
return this._fetch(`${this.baseURL}/video?id=${id}`);
|
|
789
|
+
}
|
|
790
|
+
/** Generate an avatar video (lip-synced talking head from audio + face image).
|
|
791
|
+
* Returns 202 with a job_id for async polling via avatarJobStatus(). */
|
|
792
|
+
async avatarGenerate(options) {
|
|
793
|
+
return this._request(`${this.baseURL}/avatar/generate`, {
|
|
794
|
+
method: "POST",
|
|
795
|
+
body: JSON.stringify(options)
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
/** Poll an avatar generation job by ID. */
|
|
799
|
+
async avatarJobStatus(jobId) {
|
|
800
|
+
return this._fetch(`${this.baseURL}/avatar/jobs/${jobId}`);
|
|
801
|
+
}
|
|
802
|
+
/** Poll an avatar job until it reaches a terminal state (completed/failed).
|
|
803
|
+
* Returns the final job status. Throws on failure. */
|
|
804
|
+
async pollAvatarJob(jobId, options) {
|
|
805
|
+
const interval = options?.intervalMs ?? 5e3;
|
|
806
|
+
const timeout = options?.timeoutMs ?? 3e5;
|
|
807
|
+
const deadline = Date.now() + timeout;
|
|
808
|
+
while (Date.now() < deadline) {
|
|
809
|
+
const job = await this.avatarJobStatus(jobId);
|
|
810
|
+
if (job.status === "completed") return job;
|
|
811
|
+
if (job.status === "failed") {
|
|
812
|
+
throw new TCloudError(500, job.error || `Avatar job ${jobId} failed`);
|
|
813
|
+
}
|
|
814
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
815
|
+
}
|
|
816
|
+
throw new TCloudError(408, `Avatar job ${jobId} timed out after ${timeout}ms`);
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* Watch an async job via SSE until it reaches a terminal state.
|
|
820
|
+
* Works with avatar, video, and training blueprint operators.
|
|
821
|
+
*
|
|
822
|
+
* @param jobId - The job ID returned by the creation endpoint
|
|
823
|
+
* @param options - Optional: operatorUrl override, onEvent callback
|
|
824
|
+
* @returns The final JobEvent (completed/failed/cancelled)
|
|
825
|
+
*/
|
|
826
|
+
async watchJob(jobId, options) {
|
|
827
|
+
const base = options?.operatorUrl?.replace(/\/$/, "") || this.baseURL;
|
|
828
|
+
const url = `${base}/v1/jobs/${encodeURIComponent(jobId)}/events`;
|
|
829
|
+
const timeout = options?.timeout ?? 3e5;
|
|
830
|
+
const controller = new AbortController();
|
|
831
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
832
|
+
try {
|
|
833
|
+
const watchHeaders = {
|
|
834
|
+
...this.headers,
|
|
835
|
+
Accept: "text/event-stream"
|
|
836
|
+
};
|
|
837
|
+
if (options?.operatorUrl) {
|
|
838
|
+
delete watchHeaders["Authorization"];
|
|
839
|
+
}
|
|
840
|
+
if (options?.sseToken) {
|
|
841
|
+
watchHeaders["Authorization"] = `Bearer ${options.sseToken}`;
|
|
842
|
+
}
|
|
843
|
+
const res = await proxiedFetch(this.privacy, url, {
|
|
844
|
+
headers: watchHeaders,
|
|
845
|
+
signal: controller.signal
|
|
846
|
+
}, true);
|
|
847
|
+
if (!res.ok) {
|
|
848
|
+
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
849
|
+
throw new TCloudError(res.status, err.error?.message || err.error || res.statusText);
|
|
850
|
+
}
|
|
851
|
+
const reader = res.body.getReader();
|
|
852
|
+
const decoder = new TextDecoder();
|
|
853
|
+
let buf = "";
|
|
854
|
+
const terminalStatuses = /* @__PURE__ */ new Set(["completed", "failed", "cancelled"]);
|
|
855
|
+
while (true) {
|
|
856
|
+
const { done, value } = await reader.read();
|
|
857
|
+
if (done) {
|
|
858
|
+
throw new TCloudError(502, `SSE stream ended without terminal event for job ${jobId}`);
|
|
859
|
+
}
|
|
860
|
+
buf += decoder.decode(value, { stream: true });
|
|
861
|
+
if (buf.length > 1048576) throw new TCloudError(502, "SSE buffer overflow \u2014 server sent >1MB without newline");
|
|
862
|
+
const lines = buf.split("\n");
|
|
863
|
+
buf = lines.pop() || "";
|
|
864
|
+
for (const line of lines) {
|
|
865
|
+
if (!line.startsWith("data: ")) continue;
|
|
866
|
+
const data = line.slice(6).trim();
|
|
867
|
+
if (!data || data === "[DONE]") continue;
|
|
868
|
+
let event;
|
|
869
|
+
try {
|
|
870
|
+
event = JSON.parse(data);
|
|
871
|
+
} catch {
|
|
872
|
+
continue;
|
|
873
|
+
}
|
|
874
|
+
try {
|
|
875
|
+
options?.onEvent?.(event);
|
|
876
|
+
} catch (cbErr) {
|
|
877
|
+
console.error("watchJob onEvent callback error:", cbErr);
|
|
878
|
+
}
|
|
879
|
+
if (terminalStatuses.has(event.status)) {
|
|
880
|
+
return event;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
} catch (err) {
|
|
885
|
+
if (err?.name === "AbortError") {
|
|
886
|
+
throw new TCloudError(408, `Job ${jobId} timed out after ${timeout}ms`);
|
|
887
|
+
}
|
|
888
|
+
throw err;
|
|
889
|
+
} finally {
|
|
890
|
+
clearTimeout(timer);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
// ---------------------------------------------------------------------------
|
|
894
|
+
// Vector Store (requires operator routing — X-Tangle-Service/Blueprint/Operator)
|
|
895
|
+
// ---------------------------------------------------------------------------
|
|
896
|
+
/** Create a vector collection on the operator's vector store */
|
|
897
|
+
async createCollection(options) {
|
|
898
|
+
return this._request(`${this.baseURL}/collections`, {
|
|
899
|
+
method: "POST",
|
|
900
|
+
body: JSON.stringify(options)
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
/** List collections on the operator's vector store */
|
|
904
|
+
async listCollections() {
|
|
905
|
+
return this._fetch(`${this.baseURL}/collections`);
|
|
906
|
+
}
|
|
907
|
+
/** Upsert vectors into a collection */
|
|
908
|
+
async upsertVectors(collection, vectors) {
|
|
909
|
+
return this._request(`${this.baseURL}/collections/${encodeURIComponent(collection)}/upsert`, {
|
|
910
|
+
method: "POST",
|
|
911
|
+
body: JSON.stringify({ vectors })
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
/** Similarity search in a collection */
|
|
915
|
+
async queryVectors(collection, options) {
|
|
916
|
+
return this._request(`${this.baseURL}/collections/${encodeURIComponent(collection)}/query`, {
|
|
917
|
+
method: "POST",
|
|
918
|
+
body: JSON.stringify(options)
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
/** RAG query — embed text + search collection in one call */
|
|
922
|
+
async ragQuery(options) {
|
|
923
|
+
return this._request(`${this.baseURL}/rag`, {
|
|
924
|
+
method: "POST",
|
|
925
|
+
body: JSON.stringify(options)
|
|
926
|
+
});
|
|
524
927
|
}
|
|
525
928
|
/** Search models by name, provider, or capability */
|
|
526
929
|
async searchModels(query) {
|
|
@@ -539,7 +942,68 @@ var TCloudClient = class {
|
|
|
539
942
|
const outputCost = options.outputTokens * parseFloat(model.pricing.completion);
|
|
540
943
|
return { inputCost, outputCost, total: inputCost + outputCost };
|
|
541
944
|
}
|
|
945
|
+
/**
|
|
946
|
+
* Get a pricing spectrum across resource tiers for a model.
|
|
947
|
+
*
|
|
948
|
+
* Uses REAL per-operator pricing from `operator.models[].inputPrice`.
|
|
949
|
+
* Each tier filters operators by GPU count and TEE capability, then
|
|
950
|
+
* reports the cheapest and most expensive operator for that config.
|
|
951
|
+
*
|
|
952
|
+
* @param options.model - Model ID to price (falls back to client default)
|
|
953
|
+
* @param options.tiers - Number of tiers (1-7, default 5)
|
|
954
|
+
*/
|
|
955
|
+
async pricingSpectrum(options) {
|
|
956
|
+
const requestedTiers = Math.max(1, Math.min(options.tiers ?? 5, ALL_TIERS.length));
|
|
957
|
+
const modelId = options.model || this.model;
|
|
958
|
+
const selected = selectTiers(ALL_TIERS, requestedTiers);
|
|
959
|
+
const operatorData = await this.operators();
|
|
960
|
+
const allOperators = operatorData.operators || [];
|
|
961
|
+
return selected.map((tier) => {
|
|
962
|
+
const matching = allOperators.filter((op) => {
|
|
963
|
+
if (tier.gpu > 0 && (op.gpuCount ?? 0) < tier.gpu) return false;
|
|
964
|
+
if (tier.tee && !op.teeAttested) return false;
|
|
965
|
+
return true;
|
|
966
|
+
});
|
|
967
|
+
const prices = matching.map((op) => op.models.find((m) => m.modelId === modelId)?.inputPrice).filter((p) => p != null && p > 0).sort((a, b) => a - b);
|
|
968
|
+
const cheapestPrice = prices[0];
|
|
969
|
+
const priciestPrice = prices.length > 1 ? prices[prices.length - 1] : void 0;
|
|
970
|
+
return {
|
|
971
|
+
tier: tier.name,
|
|
972
|
+
config: tier,
|
|
973
|
+
cheapestPrice,
|
|
974
|
+
priciestPrice: priciestPrice !== cheapestPrice ? priciestPrice : void 0,
|
|
975
|
+
cheapest: cheapestPrice != null ? formatPrice(cheapestPrice) : "no operators for this config",
|
|
976
|
+
priciest: priciestPrice != null && priciestPrice !== cheapestPrice ? formatPrice(priciestPrice) : void 0,
|
|
977
|
+
availableOperators: matching.length,
|
|
978
|
+
operatorsWithModel: prices.length
|
|
979
|
+
};
|
|
980
|
+
});
|
|
981
|
+
}
|
|
542
982
|
};
|
|
983
|
+
var ALL_TIERS = [
|
|
984
|
+
{ name: "cpu-only", cpu: 4, ramGb: 16, gpu: 0, tee: false },
|
|
985
|
+
{ name: "gpu", cpu: 8, ramGb: 32, gpu: 1, tee: false },
|
|
986
|
+
{ name: "gpu-tee", cpu: 8, ramGb: 32, gpu: 1, tee: true },
|
|
987
|
+
{ name: "multi-gpu", cpu: 32, ramGb: 128, gpu: 2, tee: false },
|
|
988
|
+
{ name: "multi-gpu-tee", cpu: 32, ramGb: 128, gpu: 2, tee: true },
|
|
989
|
+
{ name: "max-gpu", cpu: 64, ramGb: 256, gpu: 4, tee: false },
|
|
990
|
+
{ name: "max-gpu-tee", cpu: 64, ramGb: 256, gpu: 4, tee: true }
|
|
991
|
+
];
|
|
992
|
+
function selectTiers(all, n) {
|
|
993
|
+
if (n >= all.length) return [...all];
|
|
994
|
+
if (n <= 1) return [all[0]];
|
|
995
|
+
if (n === 2) return [all[0], all[all.length - 1]];
|
|
996
|
+
const result = [all[0]];
|
|
997
|
+
const step = (all.length - 1) / (n - 1);
|
|
998
|
+
for (let i = 1; i < n - 1; i++) {
|
|
999
|
+
result.push(all[Math.round(i * step)]);
|
|
1000
|
+
}
|
|
1001
|
+
result.push(all[all.length - 1]);
|
|
1002
|
+
return result;
|
|
1003
|
+
}
|
|
1004
|
+
function formatPrice(pricePerToken) {
|
|
1005
|
+
return `$${(pricePerToken * 1e3).toFixed(6)}/1K tokens`;
|
|
1006
|
+
}
|
|
543
1007
|
var TCloudError = class extends Error {
|
|
544
1008
|
constructor(status, message) {
|
|
545
1009
|
super(message);
|
|
@@ -842,6 +1306,7 @@ var TCloud = class _TCloud extends TCloudClient {
|
|
|
842
1306
|
};
|
|
843
1307
|
// Annotate the CommonJS export names for ESM import in node:
|
|
844
1308
|
0 && (module.exports = {
|
|
1309
|
+
PrivateRouter,
|
|
845
1310
|
TCloud,
|
|
846
1311
|
TCloudClient,
|
|
847
1312
|
TCloudError,
|