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