@tangle-network/tcloud 0.1.4 → 0.3.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-ADGH2R2R.js +263 -0
- package/dist/chunk-LN7XX5KG.js +1176 -0
- package/dist/{chunk-PILYAKCF.js → chunk-W7PUZZRE.js} +4 -2
- package/dist/cli.cjs +969 -193
- package/dist/cli.js +141 -26
- package/dist/client-DskWX_BT.d.cts +1117 -0
- package/dist/client-DskWX_BT.d.ts +1117 -0
- package/dist/index.cjs +835 -169
- package/dist/index.d.cts +5 -2
- package/dist/index.d.ts +5 -2
- package/dist/index.js +10 -4
- package/dist/instance.cjs +1434 -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 +831 -169
- package/dist/shielded.d.cts +61 -2
- package/dist/shielded.d.ts +61 -2
- package/dist/shielded.js +2 -1
- package/package.json +15 -2
- package/dist/chunk-KVXWEAK3.js +0 -771
- package/dist/shielded-BTi_OftW.d.cts +0 -513
- package/dist/shielded-BTi_OftW.d.ts +0 -513
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,25 +292,44 @@ 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 DEFAULT_PLATFORM_URL = "https://id.tangle.tools";
|
|
304
|
+
var TCloudClient = class _TCloudClient {
|
|
83
305
|
baseURL;
|
|
306
|
+
platformURL;
|
|
84
307
|
apiKey;
|
|
85
308
|
model;
|
|
86
309
|
headers;
|
|
87
310
|
spendAuthFn;
|
|
88
311
|
privacy;
|
|
89
312
|
limits;
|
|
313
|
+
retryConfig;
|
|
314
|
+
timeoutMs;
|
|
90
315
|
_totalSpent = 0;
|
|
91
316
|
_requestCount = 0;
|
|
317
|
+
privateRouter;
|
|
318
|
+
_cachedOperators = [];
|
|
319
|
+
_operatorsCachedAt = 0;
|
|
320
|
+
static OPERATORS_TTL_MS = 5 * 60 * 1e3;
|
|
92
321
|
constructor(config = {}) {
|
|
93
322
|
this.baseURL = (config.baseURL || DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
94
|
-
this.
|
|
323
|
+
this.platformURL = (config.platformURL || DEFAULT_PLATFORM_URL).replace(/\/$/, "");
|
|
324
|
+
this.apiKey = config.apiKey || process.env.TCLOUD_API_KEY;
|
|
95
325
|
this.model = config.model || "gpt-4o-mini";
|
|
96
326
|
this.privacy = config.privacy;
|
|
97
327
|
this.limits = config.limits;
|
|
328
|
+
this.retryConfig = config.retry === false ? null : { ...DEFAULT_RETRY, ...config.retry };
|
|
329
|
+
this.timeoutMs = config.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
98
330
|
this.headers = {
|
|
99
331
|
"Content-Type": "application/json",
|
|
100
|
-
"X-Tangle-Client": "tcloud-sdk/0.
|
|
332
|
+
"X-Tangle-Client": "tcloud-sdk/0.2.0"
|
|
101
333
|
};
|
|
102
334
|
if (this.apiKey) {
|
|
103
335
|
this.headers["Authorization"] = `Bearer ${this.apiKey}`;
|
|
@@ -117,6 +349,17 @@ var TCloudClient = class {
|
|
|
117
349
|
if (config.routing?.region) {
|
|
118
350
|
this.headers["X-Tangle-Region"] = config.routing.region;
|
|
119
351
|
}
|
|
352
|
+
if (config.routing?.strategy) {
|
|
353
|
+
const strategyMap = {
|
|
354
|
+
"round-robin": "round-robin",
|
|
355
|
+
"lowest-latency": "latency-aware",
|
|
356
|
+
"lowest-price": "round-robin",
|
|
357
|
+
"highest-reputation": "round-robin"
|
|
358
|
+
};
|
|
359
|
+
this.privateRouter = new PrivateRouter({
|
|
360
|
+
strategy: strategyMap[config.routing.strategy] || "round-robin"
|
|
361
|
+
});
|
|
362
|
+
}
|
|
120
363
|
}
|
|
121
364
|
/** Set the SpendAuth signer for private mode */
|
|
122
365
|
setSpendAuthSigner(fn) {
|
|
@@ -150,6 +393,25 @@ var TCloudClient = class {
|
|
|
150
393
|
if (pct >= 0.8) this.limits.onLimitWarning({ type: "total", current: this._totalSpent, limit: this.limits.maxTotalSpend });
|
|
151
394
|
}
|
|
152
395
|
}
|
|
396
|
+
/** Ensure the private router has operators loaded (with TTL-based caching) */
|
|
397
|
+
async ensureRouterOperators() {
|
|
398
|
+
if (!this.privateRouter) return;
|
|
399
|
+
const now = Date.now();
|
|
400
|
+
if (this._cachedOperators.length > 0 && now - this._operatorsCachedAt < _TCloudClient.OPERATORS_TTL_MS) {
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
const data = await this.operators();
|
|
404
|
+
this._cachedOperators = (data.operators || []).map((op) => ({
|
|
405
|
+
slug: op.slug,
|
|
406
|
+
endpointUrl: op.endpointUrl,
|
|
407
|
+
region: "",
|
|
408
|
+
reputationScore: op.reputationScore,
|
|
409
|
+
avgLatencyMs: op.avgLatencyMs,
|
|
410
|
+
models: op.models.map((m) => m.modelId)
|
|
411
|
+
}));
|
|
412
|
+
this._operatorsCachedAt = now;
|
|
413
|
+
this.privateRouter.setOperators(this._cachedOperators);
|
|
414
|
+
}
|
|
153
415
|
/** Track cost after a response, using actual pricing from response headers when available */
|
|
154
416
|
trackCost(completion, res) {
|
|
155
417
|
this._requestCount++;
|
|
@@ -169,36 +431,154 @@ var TCloudClient = class {
|
|
|
169
431
|
}
|
|
170
432
|
}
|
|
171
433
|
}
|
|
172
|
-
/**
|
|
173
|
-
|
|
434
|
+
/**
|
|
435
|
+
* Core fetch with retry + timeout. All helpers build on this.
|
|
436
|
+
* Retries on retryable status codes with exponential backoff + jitter.
|
|
437
|
+
*/
|
|
438
|
+
async _doFetch(url, init, streaming) {
|
|
439
|
+
const retry = this.retryConfig;
|
|
440
|
+
const maxAttempts = retry ? retry.maxRetries + 1 : 1;
|
|
441
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
442
|
+
const controller = new AbortController();
|
|
443
|
+
let timer;
|
|
444
|
+
if (this.timeoutMs > 0 && !streaming) {
|
|
445
|
+
timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
446
|
+
}
|
|
447
|
+
try {
|
|
448
|
+
const res = await proxiedFetch(this.privacy, url, {
|
|
449
|
+
...init,
|
|
450
|
+
signal: controller.signal
|
|
451
|
+
}, streaming);
|
|
452
|
+
if (res.ok) return res;
|
|
453
|
+
if (retry && attempt < retry.maxRetries && retry.retryableStatuses.includes(res.status)) {
|
|
454
|
+
const backoff = Math.min(
|
|
455
|
+
retry.initialBackoffMs * Math.pow(retry.multiplier, attempt),
|
|
456
|
+
retry.maxBackoffMs
|
|
457
|
+
);
|
|
458
|
+
const jitter = backoff * 0.5 * Math.random();
|
|
459
|
+
await new Promise((r) => setTimeout(r, backoff + jitter));
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
463
|
+
throw new TCloudError(res.status, err.error?.message || err.error || err.message || res.statusText);
|
|
464
|
+
} catch (e) {
|
|
465
|
+
if (e instanceof TCloudError) throw e;
|
|
466
|
+
if (retry && attempt < retry.maxRetries) {
|
|
467
|
+
const backoff = Math.min(
|
|
468
|
+
retry.initialBackoffMs * Math.pow(retry.multiplier, attempt),
|
|
469
|
+
retry.maxBackoffMs
|
|
470
|
+
);
|
|
471
|
+
await new Promise((r) => setTimeout(r, backoff));
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
if (e?.name === "AbortError") {
|
|
475
|
+
throw new TCloudError(408, `Request timed out after ${this.timeoutMs}ms`);
|
|
476
|
+
}
|
|
477
|
+
throw new TCloudError(0, e?.message || "Network error");
|
|
478
|
+
} finally {
|
|
479
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
throw new TCloudError(0, "Retry loop exhausted");
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Shared request helper for billable JSON API calls.
|
|
486
|
+
* Enforces: checkLimits → fetch with retry/timeout → error parsing → requestCount.
|
|
487
|
+
*/
|
|
488
|
+
async _request(url, init = {}) {
|
|
489
|
+
this.checkLimits();
|
|
490
|
+
const res = await this._doFetch(url, { headers: this.headers, ...init }, false);
|
|
491
|
+
this._requestCount++;
|
|
492
|
+
return res.json();
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Shared request helper for read-only/non-billable JSON API calls.
|
|
496
|
+
* No limits check, no request counting.
|
|
497
|
+
*/
|
|
498
|
+
async _fetch(url, init = {}) {
|
|
499
|
+
const res = await this._doFetch(url, { headers: this.headers, ...init }, false);
|
|
500
|
+
return res.json();
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Shared request helper for billable calls that return non-JSON (e.g. ArrayBuffer).
|
|
504
|
+
*/
|
|
505
|
+
async _requestRaw(url, init = {}) {
|
|
174
506
|
this.checkLimits();
|
|
507
|
+
const res = await this._doFetch(url, { headers: this.headers, ...init }, false);
|
|
508
|
+
this._requestCount++;
|
|
509
|
+
return res;
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Prepare headers for chat requests — operator routing + SpendAuth +
|
|
513
|
+
* bridge short-circuit headers when `options.bridge` is set.
|
|
514
|
+
* Shared between chat() and chatStream() to eliminate duplication.
|
|
515
|
+
*/
|
|
516
|
+
async _prepareChatRequest(model, bridge) {
|
|
175
517
|
const headers = { ...this.headers };
|
|
176
518
|
if (this.spendAuthFn) {
|
|
177
519
|
const auth = await this.spendAuthFn();
|
|
178
520
|
headers["X-Payment-Signature"] = JSON.stringify(auth);
|
|
179
521
|
delete headers["Authorization"];
|
|
180
522
|
}
|
|
181
|
-
|
|
523
|
+
let baseURL = this.baseURL;
|
|
524
|
+
if (this.privateRouter) {
|
|
525
|
+
await this.ensureRouterOperators();
|
|
526
|
+
const operator = this.privateRouter.selectOperator(model);
|
|
527
|
+
if (operator) {
|
|
528
|
+
baseURL = operator.endpointUrl.replace(/\/$/, "");
|
|
529
|
+
headers["X-Tangle-Operator"] = operator.slug;
|
|
530
|
+
delete headers["Authorization"];
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
if (bridge) {
|
|
534
|
+
headers["X-Bridge-Unlock"] = bridge.unlock;
|
|
535
|
+
if (bridge.resume) headers["X-Resume"] = bridge.resume;
|
|
536
|
+
if (bridge.bridgeUrl) headers["X-Bridge-Url"] = bridge.bridgeUrl;
|
|
537
|
+
if (bridge.bridgeBearer) headers["X-Bridge-Bearer"] = bridge.bridgeBearer;
|
|
538
|
+
}
|
|
539
|
+
return { headers, baseURL };
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Resolve the effective model string. When a bridge is set, rewrite to
|
|
543
|
+
* `bridge/<harness>/<model>` (or `bridge/<harness>` if no model).
|
|
544
|
+
*/
|
|
545
|
+
_effectiveModel(options) {
|
|
546
|
+
if (options.bridge) {
|
|
547
|
+
return options.bridge.model ? `bridge/${options.bridge.harness}/${options.bridge.model}` : `bridge/${options.bridge.harness}`;
|
|
548
|
+
}
|
|
549
|
+
return options.model || this.model;
|
|
550
|
+
}
|
|
551
|
+
/** Build the chat completions request body */
|
|
552
|
+
_chatBody(options, stream) {
|
|
553
|
+
return JSON.stringify({
|
|
554
|
+
model: this._effectiveModel(options),
|
|
555
|
+
messages: options.messages,
|
|
556
|
+
temperature: options.temperature,
|
|
557
|
+
max_tokens: options.maxTokens,
|
|
558
|
+
stream,
|
|
559
|
+
stop: options.stop,
|
|
560
|
+
top_p: options.topP,
|
|
561
|
+
frequency_penalty: options.frequencyPenalty,
|
|
562
|
+
presence_penalty: options.presencePenalty,
|
|
563
|
+
response_format: options.responseFormat,
|
|
564
|
+
tools: options.tools,
|
|
565
|
+
tool_choice: options.toolChoice,
|
|
566
|
+
...options.gateway ? { gateway: options.gateway } : {},
|
|
567
|
+
...options.providerOptions
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
/** Chat completion (non-streaming) */
|
|
571
|
+
async chat(options) {
|
|
572
|
+
this.checkLimits();
|
|
573
|
+
const { headers, baseURL } = await this._prepareChatRequest(
|
|
574
|
+
this._effectiveModel(options),
|
|
575
|
+
options.bridge
|
|
576
|
+
);
|
|
577
|
+
const res = await this._doFetch(`${baseURL}/chat/completions`, {
|
|
182
578
|
method: "POST",
|
|
183
579
|
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
|
-
})
|
|
580
|
+
body: this._chatBody(options, false)
|
|
197
581
|
}, 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
582
|
const completion = await res.json();
|
|
203
583
|
this.trackCost(completion, res);
|
|
204
584
|
return completion;
|
|
@@ -206,29 +586,16 @@ var TCloudClient = class {
|
|
|
206
586
|
/** Chat completion (streaming) — returns an async iterator of chunks */
|
|
207
587
|
async *chatStream(options) {
|
|
208
588
|
this.checkLimits();
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
}
|
|
215
|
-
const res = await proxiedFetch(this.privacy, `${this.baseURL}/chat/completions`, {
|
|
589
|
+
this._requestCount++;
|
|
590
|
+
const { headers, baseURL } = await this._prepareChatRequest(
|
|
591
|
+
this._effectiveModel(options),
|
|
592
|
+
options.bridge
|
|
593
|
+
);
|
|
594
|
+
const res = await this._doFetch(`${baseURL}/chat/completions`, {
|
|
216
595
|
method: "POST",
|
|
217
596
|
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
|
-
})
|
|
597
|
+
body: this._chatBody(options, true)
|
|
227
598
|
}, 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
599
|
const reader = res.body.getReader();
|
|
233
600
|
const decoder = new TextDecoder();
|
|
234
601
|
let buf = "";
|
|
@@ -236,13 +603,13 @@ var TCloudClient = class {
|
|
|
236
603
|
const { done, value } = await reader.read();
|
|
237
604
|
if (done) break;
|
|
238
605
|
buf += decoder.decode(value, { stream: true });
|
|
606
|
+
if (buf.length > 1048576) throw new TCloudError(502, "SSE buffer overflow \u2014 server sent >1MB without newline");
|
|
239
607
|
const lines = buf.split("\n");
|
|
240
608
|
buf = lines.pop() || "";
|
|
241
609
|
for (const line of lines) {
|
|
242
610
|
if (!line.startsWith("data: ")) continue;
|
|
243
611
|
const data = line.slice(6).trim();
|
|
244
612
|
if (data === "[DONE]") {
|
|
245
|
-
this._requestCount++;
|
|
246
613
|
return;
|
|
247
614
|
}
|
|
248
615
|
try {
|
|
@@ -252,6 +619,24 @@ var TCloudClient = class {
|
|
|
252
619
|
}
|
|
253
620
|
}
|
|
254
621
|
}
|
|
622
|
+
/**
|
|
623
|
+
* Bridge — scoped helper for a subscription-backed CLI harness behind
|
|
624
|
+
* the Tangle Router's cli-bridge. Returns a mini-client bound to
|
|
625
|
+
* (harness, unlock, resume) so you don't thread those through every
|
|
626
|
+
* call.
|
|
627
|
+
*
|
|
628
|
+
* ```ts
|
|
629
|
+
* const kimi = tcloud.bridge({ harness: 'kimi', model: 'kimi-for-coding', unlock: UNLOCK, resume: 'pr-42' })
|
|
630
|
+
* await kimi.ask('review this diff…')
|
|
631
|
+
* for await (const chunk of kimi.stream('continue…')) process.stdout.write(chunk)
|
|
632
|
+
* ```
|
|
633
|
+
*
|
|
634
|
+
* Sessions persist across process restarts — use the same `resume` id
|
|
635
|
+
* to land on the same CLI conversation (context intact, no replay tax).
|
|
636
|
+
*/
|
|
637
|
+
bridge(cfg) {
|
|
638
|
+
return new BridgeSession(this, cfg);
|
|
639
|
+
}
|
|
255
640
|
/** Convenience: send a single message and get the text response */
|
|
256
641
|
async ask(message, modelOrOptions) {
|
|
257
642
|
const options = typeof modelOrOptions === "string" ? { model: modelOrOptions } : modelOrOptions;
|
|
@@ -282,85 +667,112 @@ var TCloudClient = class {
|
|
|
282
667
|
}
|
|
283
668
|
/** List available models */
|
|
284
669
|
async models() {
|
|
285
|
-
const
|
|
286
|
-
if (!res.ok) throw new TCloudError(res.status, "Failed to fetch models");
|
|
287
|
-
const data = await res.json();
|
|
670
|
+
const data = await this._fetch(`${this.baseURL}/models`);
|
|
288
671
|
return data.data || [];
|
|
289
672
|
}
|
|
290
673
|
/** List active operators */
|
|
291
674
|
async operators() {
|
|
292
675
|
const apiRoot = this.baseURL.replace(/\/v1$/, "");
|
|
293
|
-
|
|
294
|
-
if (!res.ok) throw new TCloudError(res.status, "Failed to fetch operators");
|
|
295
|
-
return res.json();
|
|
676
|
+
return this._fetch(`${apiRoot}/api/operators`);
|
|
296
677
|
}
|
|
678
|
+
// ── Billing (via id.tangle.tools) ──
|
|
297
679
|
/** Get credit balance */
|
|
298
680
|
async credits() {
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
if (!res.ok) throw new TCloudError(res.status, "Failed to fetch credits");
|
|
302
|
-
return res.json();
|
|
681
|
+
const { data } = await this._fetch(`${this.platformURL}/v1/billing/balance`);
|
|
682
|
+
return data;
|
|
303
683
|
}
|
|
304
|
-
/** Add credits */
|
|
684
|
+
/** Add credits via Stripe checkout. Returns the checkout URL. */
|
|
305
685
|
async addCredits(amount) {
|
|
306
|
-
const
|
|
307
|
-
const res = await proxiedFetch(this.privacy, `${apiRoot}/api/billing`, {
|
|
686
|
+
const { data } = await this._fetch(`${this.platformURL}/v1/billing/topup`, {
|
|
308
687
|
method: "POST",
|
|
309
|
-
headers: this.headers,
|
|
310
688
|
body: JSON.stringify({ amount })
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
return res.json();
|
|
689
|
+
});
|
|
690
|
+
return data;
|
|
314
691
|
}
|
|
315
|
-
/**
|
|
316
|
-
async
|
|
317
|
-
const
|
|
318
|
-
|
|
692
|
+
/** Get transaction history */
|
|
693
|
+
async transactions(limit = 50) {
|
|
694
|
+
const { data } = await this._fetch(`${this.platformURL}/v1/billing/transactions?limit=${limit}`);
|
|
695
|
+
return data;
|
|
696
|
+
}
|
|
697
|
+
// ── API Keys (via id.tangle.tools) ──
|
|
698
|
+
/**
|
|
699
|
+
* Create a new API key.
|
|
700
|
+
* When called with an API key (not session), the new key is automatically
|
|
701
|
+
* a child of the calling key — enabling hierarchical key delegation.
|
|
702
|
+
*
|
|
703
|
+
* Pass `parentKeyId` explicitly to create a child of a specific key.
|
|
704
|
+
* Child keys inherit the parent's product scope, allowedModels, and rpmLimit
|
|
705
|
+
* if not specified. Budget cannot exceed the parent's remaining budget.
|
|
706
|
+
*/
|
|
707
|
+
async createKey(opts) {
|
|
708
|
+
const { data } = await this._fetch(`${this.platformURL}/v1/keys`, {
|
|
319
709
|
method: "POST",
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
if (!res.ok) throw new TCloudError(res.status, "Failed to create API key");
|
|
324
|
-
return res.json();
|
|
710
|
+
body: JSON.stringify(opts)
|
|
711
|
+
});
|
|
712
|
+
return data;
|
|
325
713
|
}
|
|
326
|
-
/**
|
|
327
|
-
async
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
if (!res.ok) throw new TCloudError(res.status, "Failed to fetch keys");
|
|
331
|
-
return res.json();
|
|
714
|
+
/** Get a single API key by ID */
|
|
715
|
+
async getKey(id) {
|
|
716
|
+
const { data } = await this._fetch(`${this.platformURL}/v1/keys/${id}`);
|
|
717
|
+
return data;
|
|
332
718
|
}
|
|
333
|
-
/**
|
|
719
|
+
/**
|
|
720
|
+
* List API keys.
|
|
721
|
+
* Pass `children: true` to list child keys of the calling API key.
|
|
722
|
+
*/
|
|
723
|
+
async keys(opts) {
|
|
724
|
+
const q = opts?.children ? "?children=true" : "";
|
|
725
|
+
const { data } = await this._fetch(`${this.platformURL}/v1/keys${q}`);
|
|
726
|
+
return data;
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Update an API key's limits.
|
|
730
|
+
* Can adjust budget, allowedModels, rpmLimit, expiresAt, and name.
|
|
731
|
+
*/
|
|
732
|
+
async updateKey(id, updates) {
|
|
733
|
+
const { data } = await this._fetch(`${this.platformURL}/v1/keys/${id}`, {
|
|
734
|
+
method: "PATCH",
|
|
735
|
+
body: JSON.stringify(updates)
|
|
736
|
+
});
|
|
737
|
+
return data;
|
|
738
|
+
}
|
|
739
|
+
/** Revoke an API key. If the key has children, they are also revoked recursively. */
|
|
334
740
|
async revokeKey(id) {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
}
|
|
340
|
-
|
|
741
|
+
await this._fetch(`${this.platformURL}/v1/keys/${id}`, { method: "DELETE" });
|
|
742
|
+
}
|
|
743
|
+
/** Rotate an API key — creates new key with same config, revokes old */
|
|
744
|
+
async rotateKey(id) {
|
|
745
|
+
const { data } = await this._fetch(`${this.platformURL}/v1/keys/${id}/rotate`, { method: "POST" });
|
|
746
|
+
return data;
|
|
747
|
+
}
|
|
748
|
+
// ── Projects (via id.tangle.tools) ──
|
|
749
|
+
/** Create a project for usage attribution */
|
|
750
|
+
async createProject(name, product) {
|
|
751
|
+
const { data } = await this._fetch(`${this.platformURL}/v1/projects`, {
|
|
752
|
+
method: "POST",
|
|
753
|
+
body: JSON.stringify({ name, product })
|
|
754
|
+
});
|
|
755
|
+
return data;
|
|
756
|
+
}
|
|
757
|
+
/** List projects */
|
|
758
|
+
async projects() {
|
|
759
|
+
const { data } = await this._fetch(`${this.platformURL}/v1/projects`);
|
|
760
|
+
return data;
|
|
341
761
|
}
|
|
342
762
|
/** Generate embeddings */
|
|
343
763
|
async embeddings(options) {
|
|
344
|
-
|
|
764
|
+
return this._request(`${this.baseURL}/embeddings`, {
|
|
345
765
|
method: "POST",
|
|
346
|
-
headers: this.headers,
|
|
347
766
|
body: JSON.stringify({
|
|
348
767
|
model: options.model || "text-embedding-3-small",
|
|
349
768
|
input: options.input
|
|
350
769
|
})
|
|
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();
|
|
770
|
+
});
|
|
358
771
|
}
|
|
359
772
|
/** Generate images */
|
|
360
773
|
async imageGenerate(options) {
|
|
361
|
-
|
|
774
|
+
return this._request(`${this.baseURL}/images/generations`, {
|
|
362
775
|
method: "POST",
|
|
363
|
-
headers: this.headers,
|
|
364
776
|
body: JSON.stringify({
|
|
365
777
|
model: options.model || "dall-e-3",
|
|
366
778
|
prompt: options.prompt,
|
|
@@ -369,56 +781,36 @@ var TCloudClient = class {
|
|
|
369
781
|
quality: options.quality,
|
|
370
782
|
response_format: options.response_format
|
|
371
783
|
})
|
|
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();
|
|
784
|
+
});
|
|
379
785
|
}
|
|
380
786
|
/** Rerank documents by relevance to a query */
|
|
381
787
|
async rerank(options) {
|
|
382
|
-
|
|
788
|
+
return this._request(`${this.baseURL}/rerank`, {
|
|
383
789
|
method: "POST",
|
|
384
|
-
headers: this.headers,
|
|
385
790
|
body: JSON.stringify({
|
|
386
791
|
model: options.model || "rerank-english-v3.0",
|
|
387
792
|
query: options.query,
|
|
388
793
|
documents: options.documents,
|
|
389
794
|
top_n: options.top_n
|
|
390
795
|
})
|
|
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();
|
|
796
|
+
});
|
|
398
797
|
}
|
|
399
798
|
/** Text-to-speech */
|
|
400
799
|
async speech(options) {
|
|
401
|
-
const res = await
|
|
800
|
+
const res = await this._requestRaw(`${this.baseURL}/audio/speech`, {
|
|
402
801
|
method: "POST",
|
|
403
|
-
headers: this.headers,
|
|
404
802
|
body: JSON.stringify({
|
|
405
803
|
model: options.model || "tts-1",
|
|
406
804
|
input: options.input,
|
|
407
805
|
voice: options.voice || "alloy"
|
|
408
806
|
})
|
|
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++;
|
|
807
|
+
});
|
|
415
808
|
return res.arrayBuffer();
|
|
416
809
|
}
|
|
417
810
|
/** Legacy completions endpoint */
|
|
418
811
|
async completions(options) {
|
|
419
|
-
|
|
812
|
+
return this._request(`${this.baseURL}/completions`, {
|
|
420
813
|
method: "POST",
|
|
421
|
-
headers: this.headers,
|
|
422
814
|
body: JSON.stringify({
|
|
423
815
|
model: options.model || this.model,
|
|
424
816
|
prompt: options.prompt,
|
|
@@ -427,13 +819,7 @@ var TCloudClient = class {
|
|
|
427
819
|
stop: options.stop,
|
|
428
820
|
top_p: options.topP
|
|
429
821
|
})
|
|
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();
|
|
822
|
+
});
|
|
437
823
|
}
|
|
438
824
|
/** Audio transcription (speech-to-text) */
|
|
439
825
|
async transcribe(file, options) {
|
|
@@ -444,6 +830,7 @@ var TCloudClient = class {
|
|
|
444
830
|
if (options?.prompt) formData.append("prompt", options.prompt);
|
|
445
831
|
const headers = { ...this.headers };
|
|
446
832
|
delete headers["Content-Type"];
|
|
833
|
+
this.checkLimits();
|
|
447
834
|
const res = await proxiedFetch(this.privacy, `${this.baseURL}/audio/transcriptions`, {
|
|
448
835
|
method: "POST",
|
|
449
836
|
headers,
|
|
@@ -451,75 +838,181 @@ var TCloudClient = class {
|
|
|
451
838
|
}, false);
|
|
452
839
|
if (!res.ok) {
|
|
453
840
|
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
454
|
-
throw new TCloudError(res.status, err.error?.message || err.error || res.statusText);
|
|
841
|
+
throw new TCloudError(res.status, err.error?.message || err.error || err.message || res.statusText);
|
|
455
842
|
}
|
|
456
843
|
this._requestCount++;
|
|
457
844
|
return res.json();
|
|
458
845
|
}
|
|
459
846
|
/** Create a fine-tuning job */
|
|
460
847
|
async fineTuneCreate(options) {
|
|
461
|
-
|
|
848
|
+
return this._request(`${this.baseURL}/fine_tuning/jobs`, {
|
|
462
849
|
method: "POST",
|
|
463
|
-
headers: this.headers,
|
|
464
850
|
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();
|
|
851
|
+
});
|
|
472
852
|
}
|
|
473
853
|
/** List fine-tuning jobs */
|
|
474
854
|
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();
|
|
855
|
+
return this._fetch(`${this.baseURL}/fine_tuning/jobs`);
|
|
480
856
|
}
|
|
481
857
|
/** Submit a batch of chat requests */
|
|
482
858
|
async batch(requests) {
|
|
483
|
-
|
|
859
|
+
return this._request(`${this.baseURL}/batch`, {
|
|
484
860
|
method: "POST",
|
|
485
|
-
headers: this.headers,
|
|
486
861
|
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();
|
|
862
|
+
});
|
|
493
863
|
}
|
|
494
864
|
/** Get batch job status */
|
|
495
865
|
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();
|
|
866
|
+
return this._fetch(`${this.baseURL}/batch?id=${jobId}`);
|
|
501
867
|
}
|
|
502
868
|
/** Generate video */
|
|
503
869
|
async videoGenerate(options) {
|
|
504
|
-
|
|
870
|
+
return this._request(`${this.baseURL}/video/generate`, {
|
|
505
871
|
method: "POST",
|
|
506
|
-
headers: this.headers,
|
|
507
872
|
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();
|
|
873
|
+
});
|
|
515
874
|
}
|
|
516
875
|
/** Get video generation status */
|
|
517
876
|
async videoStatus(id) {
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
877
|
+
return this._fetch(`${this.baseURL}/video?id=${id}`);
|
|
878
|
+
}
|
|
879
|
+
/** Generate an avatar video (lip-synced talking head from audio + face image).
|
|
880
|
+
* Returns 202 with a job_id for async polling via avatarJobStatus(). */
|
|
881
|
+
async avatarGenerate(options) {
|
|
882
|
+
return this._request(`${this.baseURL}/avatar/generate`, {
|
|
883
|
+
method: "POST",
|
|
884
|
+
body: JSON.stringify(options)
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
/** Poll an avatar generation job by ID. */
|
|
888
|
+
async avatarJobStatus(jobId) {
|
|
889
|
+
return this._fetch(`${this.baseURL}/avatar/jobs/${jobId}`);
|
|
890
|
+
}
|
|
891
|
+
/** Poll an avatar job until it reaches a terminal state (completed/failed).
|
|
892
|
+
* Returns the final job status. Throws on failure. */
|
|
893
|
+
async pollAvatarJob(jobId, options) {
|
|
894
|
+
const interval = options?.intervalMs ?? 5e3;
|
|
895
|
+
const timeout = options?.timeoutMs ?? 3e5;
|
|
896
|
+
const deadline = Date.now() + timeout;
|
|
897
|
+
while (Date.now() < deadline) {
|
|
898
|
+
const job = await this.avatarJobStatus(jobId);
|
|
899
|
+
if (job.status === "completed") return job;
|
|
900
|
+
if (job.status === "failed") {
|
|
901
|
+
throw new TCloudError(500, job.error || `Avatar job ${jobId} failed`);
|
|
902
|
+
}
|
|
903
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
904
|
+
}
|
|
905
|
+
throw new TCloudError(408, `Avatar job ${jobId} timed out after ${timeout}ms`);
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* Watch an async job via SSE until it reaches a terminal state.
|
|
909
|
+
* Works with avatar, video, and training blueprint operators.
|
|
910
|
+
*
|
|
911
|
+
* @param jobId - The job ID returned by the creation endpoint
|
|
912
|
+
* @param options - Optional: operatorUrl override, onEvent callback
|
|
913
|
+
* @returns The final JobEvent (completed/failed/cancelled)
|
|
914
|
+
*/
|
|
915
|
+
async watchJob(jobId, options) {
|
|
916
|
+
const base = options?.operatorUrl?.replace(/\/$/, "") || this.baseURL;
|
|
917
|
+
const url = `${base}/v1/jobs/${encodeURIComponent(jobId)}/events`;
|
|
918
|
+
const timeout = options?.timeout ?? 3e5;
|
|
919
|
+
const controller = new AbortController();
|
|
920
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
921
|
+
try {
|
|
922
|
+
const watchHeaders = {
|
|
923
|
+
...this.headers,
|
|
924
|
+
Accept: "text/event-stream"
|
|
925
|
+
};
|
|
926
|
+
if (options?.operatorUrl) {
|
|
927
|
+
delete watchHeaders["Authorization"];
|
|
928
|
+
}
|
|
929
|
+
if (options?.sseToken) {
|
|
930
|
+
watchHeaders["Authorization"] = `Bearer ${options.sseToken}`;
|
|
931
|
+
}
|
|
932
|
+
const res = await proxiedFetch(this.privacy, url, {
|
|
933
|
+
headers: watchHeaders,
|
|
934
|
+
signal: controller.signal
|
|
935
|
+
}, true);
|
|
936
|
+
if (!res.ok) {
|
|
937
|
+
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
938
|
+
throw new TCloudError(res.status, err.error?.message || err.error || res.statusText);
|
|
939
|
+
}
|
|
940
|
+
const reader = res.body.getReader();
|
|
941
|
+
const decoder = new TextDecoder();
|
|
942
|
+
let buf = "";
|
|
943
|
+
const terminalStatuses = /* @__PURE__ */ new Set(["completed", "failed", "cancelled"]);
|
|
944
|
+
while (true) {
|
|
945
|
+
const { done, value } = await reader.read();
|
|
946
|
+
if (done) {
|
|
947
|
+
throw new TCloudError(502, `SSE stream ended without terminal event for job ${jobId}`);
|
|
948
|
+
}
|
|
949
|
+
buf += decoder.decode(value, { stream: true });
|
|
950
|
+
if (buf.length > 1048576) throw new TCloudError(502, "SSE buffer overflow \u2014 server sent >1MB without newline");
|
|
951
|
+
const lines = buf.split("\n");
|
|
952
|
+
buf = lines.pop() || "";
|
|
953
|
+
for (const line of lines) {
|
|
954
|
+
if (!line.startsWith("data: ")) continue;
|
|
955
|
+
const data = line.slice(6).trim();
|
|
956
|
+
if (!data || data === "[DONE]") continue;
|
|
957
|
+
let event;
|
|
958
|
+
try {
|
|
959
|
+
event = JSON.parse(data);
|
|
960
|
+
} catch {
|
|
961
|
+
continue;
|
|
962
|
+
}
|
|
963
|
+
try {
|
|
964
|
+
options?.onEvent?.(event);
|
|
965
|
+
} catch (cbErr) {
|
|
966
|
+
console.error("watchJob onEvent callback error:", cbErr);
|
|
967
|
+
}
|
|
968
|
+
if (terminalStatuses.has(event.status)) {
|
|
969
|
+
return event;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
} catch (err) {
|
|
974
|
+
if (err?.name === "AbortError") {
|
|
975
|
+
throw new TCloudError(408, `Job ${jobId} timed out after ${timeout}ms`);
|
|
976
|
+
}
|
|
977
|
+
throw err;
|
|
978
|
+
} finally {
|
|
979
|
+
clearTimeout(timer);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
// ---------------------------------------------------------------------------
|
|
983
|
+
// Vector Store (requires operator routing — X-Tangle-Service/Blueprint/Operator)
|
|
984
|
+
// ---------------------------------------------------------------------------
|
|
985
|
+
/** Create a vector collection on the operator's vector store */
|
|
986
|
+
async createCollection(options) {
|
|
987
|
+
return this._request(`${this.baseURL}/collections`, {
|
|
988
|
+
method: "POST",
|
|
989
|
+
body: JSON.stringify(options)
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
/** List collections on the operator's vector store */
|
|
993
|
+
async listCollections() {
|
|
994
|
+
return this._fetch(`${this.baseURL}/collections`);
|
|
995
|
+
}
|
|
996
|
+
/** Upsert vectors into a collection */
|
|
997
|
+
async upsertVectors(collection, vectors) {
|
|
998
|
+
return this._request(`${this.baseURL}/collections/${encodeURIComponent(collection)}/upsert`, {
|
|
999
|
+
method: "POST",
|
|
1000
|
+
body: JSON.stringify({ vectors })
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
/** Similarity search in a collection */
|
|
1004
|
+
async queryVectors(collection, options) {
|
|
1005
|
+
return this._request(`${this.baseURL}/collections/${encodeURIComponent(collection)}/query`, {
|
|
1006
|
+
method: "POST",
|
|
1007
|
+
body: JSON.stringify(options)
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
/** RAG query — embed text + search collection in one call */
|
|
1011
|
+
async ragQuery(options) {
|
|
1012
|
+
return this._request(`${this.baseURL}/rag`, {
|
|
1013
|
+
method: "POST",
|
|
1014
|
+
body: JSON.stringify(options)
|
|
1015
|
+
});
|
|
523
1016
|
}
|
|
524
1017
|
/** Search models by name, provider, or capability */
|
|
525
1018
|
async searchModels(query) {
|
|
@@ -538,7 +1031,176 @@ var TCloudClient = class {
|
|
|
538
1031
|
const outputCost = options.outputTokens * parseFloat(model.pricing.completion);
|
|
539
1032
|
return { inputCost, outputCost, total: inputCost + outputCost };
|
|
540
1033
|
}
|
|
1034
|
+
/**
|
|
1035
|
+
* Get a pricing spectrum across resource tiers for a model.
|
|
1036
|
+
*
|
|
1037
|
+
* Uses REAL per-operator pricing from `operator.models[].inputPrice`.
|
|
1038
|
+
* Each tier filters operators by GPU count and TEE capability, then
|
|
1039
|
+
* reports the cheapest and most expensive operator for that config.
|
|
1040
|
+
*
|
|
1041
|
+
* @param options.model - Model ID to price (falls back to client default)
|
|
1042
|
+
* @param options.tiers - Number of tiers (1-7, default 5)
|
|
1043
|
+
*/
|
|
1044
|
+
async pricingSpectrum(options) {
|
|
1045
|
+
const requestedTiers = Math.max(1, Math.min(options.tiers ?? 5, ALL_TIERS.length));
|
|
1046
|
+
const modelId = options.model || this.model;
|
|
1047
|
+
const selected = selectTiers(ALL_TIERS, requestedTiers);
|
|
1048
|
+
const operatorData = await this.operators();
|
|
1049
|
+
const allOperators = operatorData.operators || [];
|
|
1050
|
+
return selected.map((tier) => {
|
|
1051
|
+
const matching = allOperators.filter((op) => {
|
|
1052
|
+
if (tier.gpu > 0 && (op.gpuCount ?? 0) < tier.gpu) return false;
|
|
1053
|
+
if (tier.tee && !op.teeAttested) return false;
|
|
1054
|
+
return true;
|
|
1055
|
+
});
|
|
1056
|
+
const prices = matching.map((op) => op.models.find((m) => m.modelId === modelId)?.inputPrice).filter((p) => p != null && p > 0).sort((a, b) => a - b);
|
|
1057
|
+
const cheapestPrice = prices[0];
|
|
1058
|
+
const priciestPrice = prices.length > 1 ? prices[prices.length - 1] : void 0;
|
|
1059
|
+
return {
|
|
1060
|
+
tier: tier.name,
|
|
1061
|
+
config: tier,
|
|
1062
|
+
cheapestPrice,
|
|
1063
|
+
priciestPrice: priciestPrice !== cheapestPrice ? priciestPrice : void 0,
|
|
1064
|
+
cheapest: cheapestPrice != null ? formatPrice(cheapestPrice) : "no operators for this config",
|
|
1065
|
+
priciest: priciestPrice != null && priciestPrice !== cheapestPrice ? formatPrice(priciestPrice) : void 0,
|
|
1066
|
+
availableOperators: matching.length,
|
|
1067
|
+
operatorsWithModel: prices.length
|
|
1068
|
+
};
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
// ── Eval ──────────────────────────────────────────────────────────────
|
|
1072
|
+
get _apiRoot() {
|
|
1073
|
+
return this.baseURL.replace(/\/v1$/, "");
|
|
1074
|
+
}
|
|
1075
|
+
async eval(opts) {
|
|
1076
|
+
return this._request(`${this._apiRoot}/api/eval`, { method: "POST", body: JSON.stringify(opts) });
|
|
1077
|
+
}
|
|
1078
|
+
async createSuite(opts) {
|
|
1079
|
+
return this._request(`${this._apiRoot}/api/eval/suites`, { method: "POST", body: JSON.stringify(opts) });
|
|
1080
|
+
}
|
|
1081
|
+
async listSuites() {
|
|
1082
|
+
return this._fetch(`${this._apiRoot}/api/eval/suites`);
|
|
1083
|
+
}
|
|
1084
|
+
async runSuite(suiteId, opts) {
|
|
1085
|
+
return this._request(`${this._apiRoot}/api/eval/suites/${suiteId}/runs`, { method: "POST", body: JSON.stringify(opts || {}) });
|
|
1086
|
+
}
|
|
1087
|
+
async listRuns(suiteId) {
|
|
1088
|
+
return this._fetch(`${this._apiRoot}/api/eval/suites/${suiteId}/runs`);
|
|
1089
|
+
}
|
|
1090
|
+
async getRun(runId) {
|
|
1091
|
+
return this._fetch(`${this._apiRoot}/api/eval/runs/${runId}`);
|
|
1092
|
+
}
|
|
1093
|
+
async setBaseline(runId) {
|
|
1094
|
+
await this._request(`${this._apiRoot}/api/eval/runs/${runId}`, { method: "PATCH", body: JSON.stringify({ baseline: true }) });
|
|
1095
|
+
}
|
|
1096
|
+
// ── Sandbox ──────────────────────────────────────────────────────────
|
|
1097
|
+
async sandboxPricing(opts) {
|
|
1098
|
+
const p = new URLSearchParams();
|
|
1099
|
+
if (opts?.cpu) p.set("cpu", String(opts.cpu));
|
|
1100
|
+
if (opts?.ram) p.set("ram", String(opts.ram));
|
|
1101
|
+
if (opts?.disk) p.set("disk", String(opts.disk));
|
|
1102
|
+
return this._fetch(`${this._apiRoot}/api/sandbox/pricing?${p}`);
|
|
1103
|
+
}
|
|
1104
|
+
async sandboxStatus() {
|
|
1105
|
+
return this._fetch(`${this._apiRoot}/api/sandbox/link-key`);
|
|
1106
|
+
}
|
|
1107
|
+
async sandboxProvision() {
|
|
1108
|
+
return this._request(`${this._apiRoot}/api/sandbox/provision`, { method: "POST" });
|
|
1109
|
+
}
|
|
1110
|
+
async sandboxCreate(opts) {
|
|
1111
|
+
return this._request(`${this._apiRoot}/api/sandbox/sessions`, { method: "POST", body: JSON.stringify(opts) });
|
|
1112
|
+
}
|
|
1113
|
+
async sandboxList() {
|
|
1114
|
+
return this._fetch(`${this._apiRoot}/api/sandbox/sessions`);
|
|
1115
|
+
}
|
|
1116
|
+
async sandboxStats(sandboxId) {
|
|
1117
|
+
return this._fetch(`${this._apiRoot}/api/sandbox/stats/${sandboxId}`);
|
|
1118
|
+
}
|
|
1119
|
+
async sandboxDestroy(sessionId) {
|
|
1120
|
+
return this._request(`${this._apiRoot}/api/sandbox/sessions/${sessionId}`, { method: "DELETE" });
|
|
1121
|
+
}
|
|
1122
|
+
// ── User Info ────────────────────────────────────────────────────────
|
|
1123
|
+
async userInfo() {
|
|
1124
|
+
return this._fetch(`${this._apiRoot}/api/auth/userinfo`);
|
|
1125
|
+
}
|
|
541
1126
|
};
|
|
1127
|
+
var ALL_TIERS = [
|
|
1128
|
+
{ name: "cpu-only", cpu: 4, ramGb: 16, gpu: 0, tee: false },
|
|
1129
|
+
{ name: "gpu", cpu: 8, ramGb: 32, gpu: 1, tee: false },
|
|
1130
|
+
{ name: "gpu-tee", cpu: 8, ramGb: 32, gpu: 1, tee: true },
|
|
1131
|
+
{ name: "multi-gpu", cpu: 32, ramGb: 128, gpu: 2, tee: false },
|
|
1132
|
+
{ name: "multi-gpu-tee", cpu: 32, ramGb: 128, gpu: 2, tee: true },
|
|
1133
|
+
{ name: "max-gpu", cpu: 64, ramGb: 256, gpu: 4, tee: false },
|
|
1134
|
+
{ name: "max-gpu-tee", cpu: 64, ramGb: 256, gpu: 4, tee: true }
|
|
1135
|
+
];
|
|
1136
|
+
var BridgeSession = class _BridgeSession {
|
|
1137
|
+
constructor(client, cfg) {
|
|
1138
|
+
this.client = client;
|
|
1139
|
+
this.cfg = cfg;
|
|
1140
|
+
}
|
|
1141
|
+
/** Full chat completion (non-streaming). */
|
|
1142
|
+
async chat(options) {
|
|
1143
|
+
return this.client.chat({ ...options, bridge: this.cfg });
|
|
1144
|
+
}
|
|
1145
|
+
/** Stream OpenAI chat.completion.chunks. */
|
|
1146
|
+
chatStream(options) {
|
|
1147
|
+
return this.client.chatStream({ ...options, bridge: this.cfg });
|
|
1148
|
+
}
|
|
1149
|
+
/** One-shot: send a string, get the assistant text. */
|
|
1150
|
+
async ask(message, extra) {
|
|
1151
|
+
const completion = await this.chat({
|
|
1152
|
+
messages: [{ role: "user", content: message }],
|
|
1153
|
+
...extra
|
|
1154
|
+
});
|
|
1155
|
+
return completion.choices[0]?.message?.content || "";
|
|
1156
|
+
}
|
|
1157
|
+
/** One-shot: send a string, stream text deltas. */
|
|
1158
|
+
async *stream(message, extra) {
|
|
1159
|
+
for await (const chunk of this.chatStream({
|
|
1160
|
+
messages: [{ role: "user", content: message }],
|
|
1161
|
+
...extra
|
|
1162
|
+
})) {
|
|
1163
|
+
const content = chunk.choices?.[0]?.delta?.content;
|
|
1164
|
+
if (content) yield content;
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
/** Turn-based: send full message history, get assistant text. */
|
|
1168
|
+
async turn(messages, extra) {
|
|
1169
|
+
const completion = await this.chat({ messages, ...extra });
|
|
1170
|
+
return completion.choices[0]?.message?.content || "";
|
|
1171
|
+
}
|
|
1172
|
+
/** Clone with a new resume id — same harness, different logical conversation. */
|
|
1173
|
+
withResume(resume) {
|
|
1174
|
+
return new _BridgeSession(this.client, { ...this.cfg, resume });
|
|
1175
|
+
}
|
|
1176
|
+
/** Clone with a different model inside the same harness. */
|
|
1177
|
+
withModel(model) {
|
|
1178
|
+
return new _BridgeSession(this.client, { ...this.cfg, model });
|
|
1179
|
+
}
|
|
1180
|
+
/** The effective model id that will land on the router (`bridge/<harness>/<model>`). */
|
|
1181
|
+
get model() {
|
|
1182
|
+
return this.cfg.model ? `bridge/${this.cfg.harness}/${this.cfg.model}` : `bridge/${this.cfg.harness}`;
|
|
1183
|
+
}
|
|
1184
|
+
/** The resume id currently bound to this session, if any. */
|
|
1185
|
+
get resume() {
|
|
1186
|
+
return this.cfg.resume;
|
|
1187
|
+
}
|
|
1188
|
+
};
|
|
1189
|
+
function selectTiers(all, n) {
|
|
1190
|
+
if (n >= all.length) return [...all];
|
|
1191
|
+
if (n <= 1) return [all[0]];
|
|
1192
|
+
if (n === 2) return [all[0], all[all.length - 1]];
|
|
1193
|
+
const result = [all[0]];
|
|
1194
|
+
const step = (all.length - 1) / (n - 1);
|
|
1195
|
+
for (let i = 1; i < n - 1; i++) {
|
|
1196
|
+
result.push(all[Math.round(i * step)]);
|
|
1197
|
+
}
|
|
1198
|
+
result.push(all[all.length - 1]);
|
|
1199
|
+
return result;
|
|
1200
|
+
}
|
|
1201
|
+
function formatPrice(pricePerToken) {
|
|
1202
|
+
return `$${(pricePerToken * 1e3).toFixed(6)}/1K tokens`;
|
|
1203
|
+
}
|
|
542
1204
|
var TCloudError = class extends Error {
|
|
543
1205
|
constructor(status, message) {
|
|
544
1206
|
super(message);
|