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