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