@tangle-network/tcloud 0.1.3 → 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-G5LUZZKT.js → chunk-STNZT6YR.js} +4 -2
- package/dist/chunk-VD4RNZOC.js +263 -0
- package/dist/cli.cjs +671 -98
- package/dist/cli.js +3 -2
- package/dist/client-CcuHG7_w.d.cts +728 -0
- package/dist/client-CcuHG7_w.d.ts +728 -0
- package/dist/index.cjs +673 -98
- 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 +671 -98
- 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-YKLHAS4E.js +0 -661
- package/dist/shielded-xDNVqK4a.d.cts +0 -404
- package/dist/shielded-xDNVqK4a.d.ts +0 -404
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,21 +310,32 @@ 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}`;
|
|
105
335
|
}
|
|
336
|
+
if (config.routing?.mode) {
|
|
337
|
+
this.headers["X-Tangle-Routing"] = config.routing.mode;
|
|
338
|
+
}
|
|
106
339
|
if (config.routing?.prefer) {
|
|
107
340
|
this.headers["X-Tangle-Operator"] = config.routing.prefer;
|
|
108
341
|
}
|
|
@@ -115,6 +348,17 @@ var TCloudClient = class {
|
|
|
115
348
|
if (config.routing?.region) {
|
|
116
349
|
this.headers["X-Tangle-Region"] = config.routing.region;
|
|
117
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
|
+
}
|
|
118
362
|
}
|
|
119
363
|
/** Set the SpendAuth signer for private mode */
|
|
120
364
|
setSpendAuthSigner(fn) {
|
|
@@ -148,6 +392,25 @@ var TCloudClient = class {
|
|
|
148
392
|
if (pct >= 0.8) this.limits.onLimitWarning({ type: "total", current: this._totalSpent, limit: this.limits.maxTotalSpend });
|
|
149
393
|
}
|
|
150
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
|
+
}
|
|
151
414
|
/** Track cost after a response, using actual pricing from response headers when available */
|
|
152
415
|
trackCost(completion, res) {
|
|
153
416
|
this._requestCount++;
|
|
@@ -167,36 +430,133 @@ var TCloudClient = class {
|
|
|
167
430
|
}
|
|
168
431
|
}
|
|
169
432
|
}
|
|
170
|
-
/**
|
|
171
|
-
|
|
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 = {}) {
|
|
172
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) {
|
|
173
515
|
const headers = { ...this.headers };
|
|
174
516
|
if (this.spendAuthFn) {
|
|
175
517
|
const auth = await this.spendAuthFn();
|
|
176
518
|
headers["X-Payment-Signature"] = JSON.stringify(auth);
|
|
177
519
|
delete headers["Authorization"];
|
|
178
520
|
}
|
|
179
|
-
|
|
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`, {
|
|
180
556
|
method: "POST",
|
|
181
557
|
headers,
|
|
182
|
-
body:
|
|
183
|
-
model: options.model || this.model,
|
|
184
|
-
messages: options.messages,
|
|
185
|
-
temperature: options.temperature,
|
|
186
|
-
max_tokens: options.maxTokens,
|
|
187
|
-
stream: false,
|
|
188
|
-
stop: options.stop,
|
|
189
|
-
top_p: options.topP,
|
|
190
|
-
frequency_penalty: options.frequencyPenalty,
|
|
191
|
-
presence_penalty: options.presencePenalty,
|
|
192
|
-
response_format: options.responseFormat,
|
|
193
|
-
tools: options.tools
|
|
194
|
-
})
|
|
558
|
+
body: this._chatBody(options, false)
|
|
195
559
|
}, false);
|
|
196
|
-
if (!res.ok) {
|
|
197
|
-
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
198
|
-
throw new TCloudError(res.status, err.error?.message || err.error || err.message || res.statusText);
|
|
199
|
-
}
|
|
200
560
|
const completion = await res.json();
|
|
201
561
|
this.trackCost(completion, res);
|
|
202
562
|
return completion;
|
|
@@ -204,29 +564,13 @@ var TCloudClient = class {
|
|
|
204
564
|
/** Chat completion (streaming) — returns an async iterator of chunks */
|
|
205
565
|
async *chatStream(options) {
|
|
206
566
|
this.checkLimits();
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
headers["X-Payment-Signature"] = JSON.stringify(auth);
|
|
211
|
-
delete headers["Authorization"];
|
|
212
|
-
}
|
|
213
|
-
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`, {
|
|
214
570
|
method: "POST",
|
|
215
571
|
headers,
|
|
216
|
-
body:
|
|
217
|
-
model: options.model || this.model,
|
|
218
|
-
messages: options.messages,
|
|
219
|
-
temperature: options.temperature,
|
|
220
|
-
max_tokens: options.maxTokens,
|
|
221
|
-
stream: true,
|
|
222
|
-
stop: options.stop,
|
|
223
|
-
top_p: options.topP
|
|
224
|
-
})
|
|
572
|
+
body: this._chatBody(options, true)
|
|
225
573
|
}, true);
|
|
226
|
-
if (!res.ok) {
|
|
227
|
-
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
228
|
-
throw new TCloudError(res.status, err.error || err.message || res.statusText);
|
|
229
|
-
}
|
|
230
574
|
const reader = res.body.getReader();
|
|
231
575
|
const decoder = new TextDecoder();
|
|
232
576
|
let buf = "";
|
|
@@ -234,13 +578,13 @@ var TCloudClient = class {
|
|
|
234
578
|
const { done, value } = await reader.read();
|
|
235
579
|
if (done) break;
|
|
236
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");
|
|
237
582
|
const lines = buf.split("\n");
|
|
238
583
|
buf = lines.pop() || "";
|
|
239
584
|
for (const line of lines) {
|
|
240
585
|
if (!line.startsWith("data: ")) continue;
|
|
241
586
|
const data = line.slice(6).trim();
|
|
242
587
|
if (data === "[DONE]") {
|
|
243
|
-
this._requestCount++;
|
|
244
588
|
return;
|
|
245
589
|
}
|
|
246
590
|
try {
|
|
@@ -280,53 +624,39 @@ var TCloudClient = class {
|
|
|
280
624
|
}
|
|
281
625
|
/** List available models */
|
|
282
626
|
async models() {
|
|
283
|
-
const
|
|
284
|
-
if (!res.ok) throw new TCloudError(res.status, "Failed to fetch models");
|
|
285
|
-
const data = await res.json();
|
|
627
|
+
const data = await this._fetch(`${this.baseURL}/models`);
|
|
286
628
|
return data.data || [];
|
|
287
629
|
}
|
|
288
630
|
/** List active operators */
|
|
289
631
|
async operators() {
|
|
290
632
|
const apiRoot = this.baseURL.replace(/\/v1$/, "");
|
|
291
|
-
|
|
292
|
-
if (!res.ok) throw new TCloudError(res.status, "Failed to fetch operators");
|
|
293
|
-
return res.json();
|
|
633
|
+
return this._fetch(`${apiRoot}/api/operators`);
|
|
294
634
|
}
|
|
295
635
|
/** Get credit balance */
|
|
296
636
|
async credits() {
|
|
297
637
|
const apiRoot = this.baseURL.replace(/\/v1$/, "");
|
|
298
|
-
|
|
299
|
-
if (!res.ok) throw new TCloudError(res.status, "Failed to fetch credits");
|
|
300
|
-
return res.json();
|
|
638
|
+
return this._fetch(`${apiRoot}/api/billing`);
|
|
301
639
|
}
|
|
302
640
|
/** Add credits */
|
|
303
641
|
async addCredits(amount) {
|
|
304
642
|
const apiRoot = this.baseURL.replace(/\/v1$/, "");
|
|
305
|
-
|
|
643
|
+
return this._fetch(`${apiRoot}/api/billing`, {
|
|
306
644
|
method: "POST",
|
|
307
|
-
headers: this.headers,
|
|
308
645
|
body: JSON.stringify({ amount })
|
|
309
|
-
}
|
|
310
|
-
if (!res.ok) throw new TCloudError(res.status, "Failed to add credits");
|
|
311
|
-
return res.json();
|
|
646
|
+
});
|
|
312
647
|
}
|
|
313
648
|
/** Create a new API key */
|
|
314
649
|
async createKey(name) {
|
|
315
650
|
const apiRoot = this.baseURL.replace(/\/v1$/, "");
|
|
316
|
-
|
|
651
|
+
return this._fetch(`${apiRoot}/api/keys`, {
|
|
317
652
|
method: "POST",
|
|
318
|
-
headers: this.headers,
|
|
319
653
|
body: JSON.stringify({ name })
|
|
320
|
-
}
|
|
321
|
-
if (!res.ok) throw new TCloudError(res.status, "Failed to create API key");
|
|
322
|
-
return res.json();
|
|
654
|
+
});
|
|
323
655
|
}
|
|
324
656
|
/** List API keys */
|
|
325
657
|
async keys() {
|
|
326
658
|
const apiRoot = this.baseURL.replace(/\/v1$/, "");
|
|
327
|
-
|
|
328
|
-
if (!res.ok) throw new TCloudError(res.status, "Failed to fetch keys");
|
|
329
|
-
return res.json();
|
|
659
|
+
return this._fetch(`${apiRoot}/api/keys`);
|
|
330
660
|
}
|
|
331
661
|
/** Revoke an API key */
|
|
332
662
|
async revokeKey(id) {
|
|
@@ -335,30 +665,25 @@ var TCloudClient = class {
|
|
|
335
665
|
method: "DELETE",
|
|
336
666
|
headers: this.headers
|
|
337
667
|
}, false);
|
|
338
|
-
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
|
+
}
|
|
339
672
|
}
|
|
340
673
|
/** Generate embeddings */
|
|
341
674
|
async embeddings(options) {
|
|
342
|
-
|
|
675
|
+
return this._request(`${this.baseURL}/embeddings`, {
|
|
343
676
|
method: "POST",
|
|
344
|
-
headers: this.headers,
|
|
345
677
|
body: JSON.stringify({
|
|
346
678
|
model: options.model || "text-embedding-3-small",
|
|
347
679
|
input: options.input
|
|
348
680
|
})
|
|
349
|
-
}
|
|
350
|
-
if (!res.ok) {
|
|
351
|
-
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
352
|
-
throw new TCloudError(res.status, err.error?.message || err.error || res.statusText);
|
|
353
|
-
}
|
|
354
|
-
this._requestCount++;
|
|
355
|
-
return res.json();
|
|
681
|
+
});
|
|
356
682
|
}
|
|
357
683
|
/** Generate images */
|
|
358
684
|
async imageGenerate(options) {
|
|
359
|
-
|
|
685
|
+
return this._request(`${this.baseURL}/images/generations`, {
|
|
360
686
|
method: "POST",
|
|
361
|
-
headers: this.headers,
|
|
362
687
|
body: JSON.stringify({
|
|
363
688
|
model: options.model || "dall-e-3",
|
|
364
689
|
prompt: options.prompt,
|
|
@@ -367,50 +692,238 @@ var TCloudClient = class {
|
|
|
367
692
|
quality: options.quality,
|
|
368
693
|
response_format: options.response_format
|
|
369
694
|
})
|
|
370
|
-
}
|
|
371
|
-
if (!res.ok) {
|
|
372
|
-
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
373
|
-
throw new TCloudError(res.status, err.error?.message || err.error || res.statusText);
|
|
374
|
-
}
|
|
375
|
-
this._requestCount++;
|
|
376
|
-
return res.json();
|
|
695
|
+
});
|
|
377
696
|
}
|
|
378
697
|
/** Rerank documents by relevance to a query */
|
|
379
698
|
async rerank(options) {
|
|
380
|
-
|
|
699
|
+
return this._request(`${this.baseURL}/rerank`, {
|
|
381
700
|
method: "POST",
|
|
382
|
-
headers: this.headers,
|
|
383
701
|
body: JSON.stringify({
|
|
384
702
|
model: options.model || "rerank-english-v3.0",
|
|
385
703
|
query: options.query,
|
|
386
704
|
documents: options.documents,
|
|
387
705
|
top_n: options.top_n
|
|
388
706
|
})
|
|
389
|
-
}
|
|
390
|
-
if (!res.ok) {
|
|
391
|
-
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
392
|
-
throw new TCloudError(res.status, err.error?.message || err.error || res.statusText);
|
|
393
|
-
}
|
|
394
|
-
this._requestCount++;
|
|
395
|
-
return res.json();
|
|
707
|
+
});
|
|
396
708
|
}
|
|
397
709
|
/** Text-to-speech */
|
|
398
710
|
async speech(options) {
|
|
399
|
-
const res = await
|
|
711
|
+
const res = await this._requestRaw(`${this.baseURL}/audio/speech`, {
|
|
400
712
|
method: "POST",
|
|
401
|
-
headers: this.headers,
|
|
402
713
|
body: JSON.stringify({
|
|
403
714
|
model: options.model || "tts-1",
|
|
404
715
|
input: options.input,
|
|
405
716
|
voice: options.voice || "alloy"
|
|
406
717
|
})
|
|
718
|
+
});
|
|
719
|
+
return res.arrayBuffer();
|
|
720
|
+
}
|
|
721
|
+
/** Legacy completions endpoint */
|
|
722
|
+
async completions(options) {
|
|
723
|
+
return this._request(`${this.baseURL}/completions`, {
|
|
724
|
+
method: "POST",
|
|
725
|
+
body: JSON.stringify({
|
|
726
|
+
model: options.model || this.model,
|
|
727
|
+
prompt: options.prompt,
|
|
728
|
+
temperature: options.temperature,
|
|
729
|
+
max_tokens: options.maxTokens,
|
|
730
|
+
stop: options.stop,
|
|
731
|
+
top_p: options.topP
|
|
732
|
+
})
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
/** Audio transcription (speech-to-text) */
|
|
736
|
+
async transcribe(file, options) {
|
|
737
|
+
const formData = new FormData();
|
|
738
|
+
formData.append("file", file, "audio.webm");
|
|
739
|
+
formData.append("model", options?.model || "whisper-1");
|
|
740
|
+
if (options?.language) formData.append("language", options.language);
|
|
741
|
+
if (options?.prompt) formData.append("prompt", options.prompt);
|
|
742
|
+
const headers = { ...this.headers };
|
|
743
|
+
delete headers["Content-Type"];
|
|
744
|
+
this.checkLimits();
|
|
745
|
+
const res = await proxiedFetch(this.privacy, `${this.baseURL}/audio/transcriptions`, {
|
|
746
|
+
method: "POST",
|
|
747
|
+
headers,
|
|
748
|
+
body: formData
|
|
407
749
|
}, false);
|
|
408
750
|
if (!res.ok) {
|
|
409
751
|
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
410
|
-
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);
|
|
411
753
|
}
|
|
412
754
|
this._requestCount++;
|
|
413
|
-
return res.
|
|
755
|
+
return res.json();
|
|
756
|
+
}
|
|
757
|
+
/** Create a fine-tuning job */
|
|
758
|
+
async fineTuneCreate(options) {
|
|
759
|
+
return this._request(`${this.baseURL}/fine_tuning/jobs`, {
|
|
760
|
+
method: "POST",
|
|
761
|
+
body: JSON.stringify(options)
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
/** List fine-tuning jobs */
|
|
765
|
+
async fineTuneList() {
|
|
766
|
+
return this._fetch(`${this.baseURL}/fine_tuning/jobs`);
|
|
767
|
+
}
|
|
768
|
+
/** Submit a batch of chat requests */
|
|
769
|
+
async batch(requests) {
|
|
770
|
+
return this._request(`${this.baseURL}/batch`, {
|
|
771
|
+
method: "POST",
|
|
772
|
+
body: JSON.stringify({ requests })
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
/** Get batch job status */
|
|
776
|
+
async batchStatus(jobId) {
|
|
777
|
+
return this._fetch(`${this.baseURL}/batch?id=${jobId}`);
|
|
778
|
+
}
|
|
779
|
+
/** Generate video */
|
|
780
|
+
async videoGenerate(options) {
|
|
781
|
+
return this._request(`${this.baseURL}/video/generate`, {
|
|
782
|
+
method: "POST",
|
|
783
|
+
body: JSON.stringify(options)
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
/** Get video generation status */
|
|
787
|
+
async videoStatus(id) {
|
|
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
|
+
});
|
|
414
927
|
}
|
|
415
928
|
/** Search models by name, provider, or capability */
|
|
416
929
|
async searchModels(query) {
|
|
@@ -429,7 +942,68 @@ var TCloudClient = class {
|
|
|
429
942
|
const outputCost = options.outputTokens * parseFloat(model.pricing.completion);
|
|
430
943
|
return { inputCost, outputCost, total: inputCost + outputCost };
|
|
431
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
|
+
}
|
|
432
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
|
+
}
|
|
433
1007
|
var TCloudError = class extends Error {
|
|
434
1008
|
constructor(status, message) {
|
|
435
1009
|
super(message);
|
|
@@ -732,6 +1306,7 @@ var TCloud = class _TCloud extends TCloudClient {
|
|
|
732
1306
|
};
|
|
733
1307
|
// Annotate the CommonJS export names for ESM import in node:
|
|
734
1308
|
0 && (module.exports = {
|
|
1309
|
+
PrivateRouter,
|
|
735
1310
|
TCloud,
|
|
736
1311
|
TCloudClient,
|
|
737
1312
|
TCloudError,
|