@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/cli.cjs
CHANGED
|
@@ -26,6 +26,219 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
26
26
|
// src/cli.ts
|
|
27
27
|
var import_commander = require("commander");
|
|
28
28
|
|
|
29
|
+
// src/private-router.ts
|
|
30
|
+
function secureRandom() {
|
|
31
|
+
const arr = new Uint32Array(1);
|
|
32
|
+
crypto.getRandomValues(arr);
|
|
33
|
+
return arr[0] / (4294967295 + 1);
|
|
34
|
+
}
|
|
35
|
+
var PrivateRouter = class {
|
|
36
|
+
config;
|
|
37
|
+
operators = [];
|
|
38
|
+
usage = /* @__PURE__ */ new Map();
|
|
39
|
+
currentIndex = 0;
|
|
40
|
+
totalRequests = 0;
|
|
41
|
+
constructor(config = {}) {
|
|
42
|
+
this.config = {
|
|
43
|
+
strategy: config.strategy || "round-robin",
|
|
44
|
+
maxRequestsPerOperator: config.maxRequestsPerOperator || 5,
|
|
45
|
+
minOperators: config.minOperators || 3,
|
|
46
|
+
preferRegions: config.preferRegions,
|
|
47
|
+
excludeOperators: config.excludeOperators,
|
|
48
|
+
summarizeOnSwitch: config.summarizeOnSwitch ?? false
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/** Set the available operator pool */
|
|
52
|
+
setOperators(operators) {
|
|
53
|
+
let filtered = operators.filter(
|
|
54
|
+
(o) => !this.config.excludeOperators?.includes(o.slug)
|
|
55
|
+
);
|
|
56
|
+
if (this.config.preferRegions?.length) {
|
|
57
|
+
filtered.sort((a, b) => {
|
|
58
|
+
const aPreferred = this.config.preferRegions.includes(a.region) ? 0 : 1;
|
|
59
|
+
const bPreferred = this.config.preferRegions.includes(b.region) ? 0 : 1;
|
|
60
|
+
return aPreferred - bPreferred;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
this.operators = filtered;
|
|
64
|
+
}
|
|
65
|
+
/** Select the next operator for a request */
|
|
66
|
+
selectOperator(model) {
|
|
67
|
+
const eligible = this.operators.filter((o) => o.models.includes(model));
|
|
68
|
+
if (eligible.length === 0) return null;
|
|
69
|
+
if (eligible.length < this.config.minOperators) {
|
|
70
|
+
console.warn(
|
|
71
|
+
`[PrivateRouter] Only ${eligible.length} eligible operator(s) for model "${model}", but minOperators requires ${this.config.minOperators}. Refusing to route.`
|
|
72
|
+
);
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
this.totalRequests++;
|
|
76
|
+
switch (this.config.strategy) {
|
|
77
|
+
case "round-robin":
|
|
78
|
+
return this.roundRobin(eligible);
|
|
79
|
+
case "random":
|
|
80
|
+
return this.random(eligible);
|
|
81
|
+
case "geo-distributed":
|
|
82
|
+
return this.geoDistributed(eligible);
|
|
83
|
+
case "min-exposure":
|
|
84
|
+
return this.minExposure(eligible);
|
|
85
|
+
case "latency-aware":
|
|
86
|
+
return this.latencyAware(eligible);
|
|
87
|
+
default:
|
|
88
|
+
return this.roundRobin(eligible);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/** Should we summarize context before this request? (operator is changing) */
|
|
92
|
+
shouldSummarize(model) {
|
|
93
|
+
if (!this.config.summarizeOnSwitch) return false;
|
|
94
|
+
const next = this.peekNextOperator(model);
|
|
95
|
+
const last = this.getLastUsedOperator();
|
|
96
|
+
return next !== null && last !== null && next.slug !== last.slug;
|
|
97
|
+
}
|
|
98
|
+
/** Get privacy stats */
|
|
99
|
+
getStats() {
|
|
100
|
+
return {
|
|
101
|
+
totalRequests: this.totalRequests,
|
|
102
|
+
operatorsUsed: this.usage.size,
|
|
103
|
+
operatorBreakdown: Array.from(this.usage.values()).map((u) => ({
|
|
104
|
+
slug: u.slug,
|
|
105
|
+
requests: u.requestCount,
|
|
106
|
+
lastUsed: u.lastUsedAt
|
|
107
|
+
})),
|
|
108
|
+
strategy: this.config.strategy
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
// ─── Strategies ────────────────────────────────────────────
|
|
112
|
+
roundRobin(eligible) {
|
|
113
|
+
const op = eligible[this.currentIndex % eligible.length];
|
|
114
|
+
this.currentIndex++;
|
|
115
|
+
this.recordUsage(op);
|
|
116
|
+
return op;
|
|
117
|
+
}
|
|
118
|
+
random(eligible) {
|
|
119
|
+
const idx = Math.floor(secureRandom() * eligible.length);
|
|
120
|
+
const op = eligible[idx];
|
|
121
|
+
this.recordUsage(op);
|
|
122
|
+
return op;
|
|
123
|
+
}
|
|
124
|
+
geoDistributed(eligible) {
|
|
125
|
+
const regionUsage = /* @__PURE__ */ new Map();
|
|
126
|
+
for (const op2 of eligible) {
|
|
127
|
+
const usage = this.usage.get(op2.slug)?.requestCount || 0;
|
|
128
|
+
const current = regionUsage.get(op2.region) || 0;
|
|
129
|
+
regionUsage.set(op2.region, current + usage);
|
|
130
|
+
}
|
|
131
|
+
const sortedRegions = [...regionUsage.entries()].sort((a, b) => a[1] - b[1]);
|
|
132
|
+
const targetRegion = sortedRegions[0]?.[0];
|
|
133
|
+
const regionOps = eligible.filter((o) => o.region === targetRegion);
|
|
134
|
+
const op = regionOps[Math.floor(secureRandom() * regionOps.length)] || eligible[0];
|
|
135
|
+
this.recordUsage(op);
|
|
136
|
+
return op;
|
|
137
|
+
}
|
|
138
|
+
minExposure(eligible) {
|
|
139
|
+
const lastUsed = this.getLastUsedOperator();
|
|
140
|
+
if (lastUsed) {
|
|
141
|
+
const lastUsage = this.usage.get(lastUsed.slug);
|
|
142
|
+
const others = eligible.filter((o) => o.slug !== lastUsed.slug);
|
|
143
|
+
if (others.length > 0 && lastUsage && lastUsage.requestCount > 0) {
|
|
144
|
+
const sorted2 = others.sort(
|
|
145
|
+
(a, b) => (this.usage.get(a.slug)?.requestCount || 0) - (this.usage.get(b.slug)?.requestCount || 0)
|
|
146
|
+
);
|
|
147
|
+
const op2 = sorted2[0];
|
|
148
|
+
this.recordUsage(op2);
|
|
149
|
+
return op2;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const sorted = [...eligible].sort(
|
|
153
|
+
(a, b) => (this.usage.get(a.slug)?.requestCount || 0) - (this.usage.get(b.slug)?.requestCount || 0)
|
|
154
|
+
);
|
|
155
|
+
const op = sorted[0];
|
|
156
|
+
this.recordUsage(op);
|
|
157
|
+
return op;
|
|
158
|
+
}
|
|
159
|
+
latencyAware(eligible) {
|
|
160
|
+
const weights = eligible.map((o) => {
|
|
161
|
+
const latencyWeight = 1 / Math.max(o.avgLatencyMs, 10);
|
|
162
|
+
const usagePenalty = (this.usage.get(o.slug)?.requestCount || 0) * 0.1;
|
|
163
|
+
return Math.max(latencyWeight - usagePenalty, 0.01);
|
|
164
|
+
});
|
|
165
|
+
const totalWeight = weights.reduce((s, w) => s + w, 0);
|
|
166
|
+
let r = secureRandom() * totalWeight;
|
|
167
|
+
for (let i = 0; i < eligible.length; i++) {
|
|
168
|
+
r -= weights[i];
|
|
169
|
+
if (r <= 0) {
|
|
170
|
+
this.recordUsage(eligible[i]);
|
|
171
|
+
return eligible[i];
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const op = eligible[eligible.length - 1];
|
|
175
|
+
this.recordUsage(op);
|
|
176
|
+
return op;
|
|
177
|
+
}
|
|
178
|
+
// ─── Helpers ───────────────────────────────────────────────
|
|
179
|
+
recordUsage(op) {
|
|
180
|
+
const existing = this.usage.get(op.slug);
|
|
181
|
+
this.usage.set(op.slug, {
|
|
182
|
+
slug: op.slug,
|
|
183
|
+
requestCount: (existing?.requestCount || 0) + 1,
|
|
184
|
+
lastUsedAt: Date.now()
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
getLastUsedOperator() {
|
|
188
|
+
let latest = null;
|
|
189
|
+
for (const u of this.usage.values()) {
|
|
190
|
+
if (!latest || u.lastUsedAt > latest.lastUsedAt) latest = u;
|
|
191
|
+
}
|
|
192
|
+
if (!latest) return null;
|
|
193
|
+
return this.operators.find((o) => o.slug === latest.slug) || null;
|
|
194
|
+
}
|
|
195
|
+
peekNextOperator(model) {
|
|
196
|
+
const eligible = this.operators.filter((o) => o.models.includes(model));
|
|
197
|
+
if (eligible.length === 0) return null;
|
|
198
|
+
if (eligible.length < this.config.minOperators) return null;
|
|
199
|
+
const last = this.getLastUsedOperator();
|
|
200
|
+
switch (this.config.strategy) {
|
|
201
|
+
case "round-robin":
|
|
202
|
+
return eligible[this.currentIndex % eligible.length];
|
|
203
|
+
case "min-exposure": {
|
|
204
|
+
if (last) {
|
|
205
|
+
const lastUsage = this.usage.get(last.slug);
|
|
206
|
+
const others = eligible.filter((o) => o.slug !== last.slug);
|
|
207
|
+
if (others.length > 0 && lastUsage && lastUsage.requestCount > 0) {
|
|
208
|
+
const sorted2 = others.sort(
|
|
209
|
+
(a, b) => (this.usage.get(a.slug)?.requestCount || 0) - (this.usage.get(b.slug)?.requestCount || 0)
|
|
210
|
+
);
|
|
211
|
+
return sorted2[0];
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const sorted = [...eligible].sort(
|
|
215
|
+
(a, b) => (this.usage.get(a.slug)?.requestCount || 0) - (this.usage.get(b.slug)?.requestCount || 0)
|
|
216
|
+
);
|
|
217
|
+
return sorted[0];
|
|
218
|
+
}
|
|
219
|
+
case "geo-distributed": {
|
|
220
|
+
const regionUsage = /* @__PURE__ */ new Map();
|
|
221
|
+
for (const op of eligible) {
|
|
222
|
+
const usage = this.usage.get(op.slug)?.requestCount || 0;
|
|
223
|
+
const current = regionUsage.get(op.region) || 0;
|
|
224
|
+
regionUsage.set(op.region, current + usage);
|
|
225
|
+
}
|
|
226
|
+
const sortedRegions = [...regionUsage.entries()].sort((a, b) => a[1] - b[1]);
|
|
227
|
+
const targetRegion = sortedRegions[0]?.[0];
|
|
228
|
+
const regionOps = eligible.filter((o) => o.region === targetRegion);
|
|
229
|
+
return regionOps[0] || eligible[0];
|
|
230
|
+
}
|
|
231
|
+
case "random":
|
|
232
|
+
case "latency-aware":
|
|
233
|
+
default:
|
|
234
|
+
if (last && eligible.length > 1) {
|
|
235
|
+
return eligible.find((o) => o.slug !== last.slug) || eligible[0];
|
|
236
|
+
}
|
|
237
|
+
return eligible[0];
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
|
|
29
242
|
// src/client.ts
|
|
30
243
|
var DEFAULT_BASE_URL = "https://router.tangle.tools/v1";
|
|
31
244
|
async function proxiedFetch(privacy, url, init, streaming) {
|
|
@@ -66,25 +279,44 @@ async function proxiedFetch(privacy, url, init, streaming) {
|
|
|
66
279
|
}
|
|
67
280
|
return fetch(url, init);
|
|
68
281
|
}
|
|
69
|
-
var
|
|
282
|
+
var DEFAULT_RETRY = {
|
|
283
|
+
maxRetries: 3,
|
|
284
|
+
initialBackoffMs: 500,
|
|
285
|
+
maxBackoffMs: 3e4,
|
|
286
|
+
multiplier: 2,
|
|
287
|
+
retryableStatuses: [429, 500, 502, 503, 504]
|
|
288
|
+
};
|
|
289
|
+
var DEFAULT_TIMEOUT_MS = 6e4;
|
|
290
|
+
var DEFAULT_PLATFORM_URL = "https://id.tangle.tools";
|
|
291
|
+
var TCloudClient = class _TCloudClient {
|
|
70
292
|
baseURL;
|
|
293
|
+
platformURL;
|
|
71
294
|
apiKey;
|
|
72
295
|
model;
|
|
73
296
|
headers;
|
|
74
297
|
spendAuthFn;
|
|
75
298
|
privacy;
|
|
76
299
|
limits;
|
|
300
|
+
retryConfig;
|
|
301
|
+
timeoutMs;
|
|
77
302
|
_totalSpent = 0;
|
|
78
303
|
_requestCount = 0;
|
|
304
|
+
privateRouter;
|
|
305
|
+
_cachedOperators = [];
|
|
306
|
+
_operatorsCachedAt = 0;
|
|
307
|
+
static OPERATORS_TTL_MS = 5 * 60 * 1e3;
|
|
79
308
|
constructor(config = {}) {
|
|
80
309
|
this.baseURL = (config.baseURL || DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
81
|
-
this.
|
|
310
|
+
this.platformURL = (config.platformURL || DEFAULT_PLATFORM_URL).replace(/\/$/, "");
|
|
311
|
+
this.apiKey = config.apiKey || process.env.TCLOUD_API_KEY;
|
|
82
312
|
this.model = config.model || "gpt-4o-mini";
|
|
83
313
|
this.privacy = config.privacy;
|
|
84
314
|
this.limits = config.limits;
|
|
315
|
+
this.retryConfig = config.retry === false ? null : { ...DEFAULT_RETRY, ...config.retry };
|
|
316
|
+
this.timeoutMs = config.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
85
317
|
this.headers = {
|
|
86
318
|
"Content-Type": "application/json",
|
|
87
|
-
"X-Tangle-Client": "tcloud-sdk/0.
|
|
319
|
+
"X-Tangle-Client": "tcloud-sdk/0.2.0"
|
|
88
320
|
};
|
|
89
321
|
if (this.apiKey) {
|
|
90
322
|
this.headers["Authorization"] = `Bearer ${this.apiKey}`;
|
|
@@ -104,6 +336,17 @@ var TCloudClient = class {
|
|
|
104
336
|
if (config.routing?.region) {
|
|
105
337
|
this.headers["X-Tangle-Region"] = config.routing.region;
|
|
106
338
|
}
|
|
339
|
+
if (config.routing?.strategy) {
|
|
340
|
+
const strategyMap = {
|
|
341
|
+
"round-robin": "round-robin",
|
|
342
|
+
"lowest-latency": "latency-aware",
|
|
343
|
+
"lowest-price": "round-robin",
|
|
344
|
+
"highest-reputation": "round-robin"
|
|
345
|
+
};
|
|
346
|
+
this.privateRouter = new PrivateRouter({
|
|
347
|
+
strategy: strategyMap[config.routing.strategy] || "round-robin"
|
|
348
|
+
});
|
|
349
|
+
}
|
|
107
350
|
}
|
|
108
351
|
/** Set the SpendAuth signer for private mode */
|
|
109
352
|
setSpendAuthSigner(fn) {
|
|
@@ -137,6 +380,25 @@ var TCloudClient = class {
|
|
|
137
380
|
if (pct >= 0.8) this.limits.onLimitWarning({ type: "total", current: this._totalSpent, limit: this.limits.maxTotalSpend });
|
|
138
381
|
}
|
|
139
382
|
}
|
|
383
|
+
/** Ensure the private router has operators loaded (with TTL-based caching) */
|
|
384
|
+
async ensureRouterOperators() {
|
|
385
|
+
if (!this.privateRouter) return;
|
|
386
|
+
const now = Date.now();
|
|
387
|
+
if (this._cachedOperators.length > 0 && now - this._operatorsCachedAt < _TCloudClient.OPERATORS_TTL_MS) {
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
const data = await this.operators();
|
|
391
|
+
this._cachedOperators = (data.operators || []).map((op) => ({
|
|
392
|
+
slug: op.slug,
|
|
393
|
+
endpointUrl: op.endpointUrl,
|
|
394
|
+
region: "",
|
|
395
|
+
reputationScore: op.reputationScore,
|
|
396
|
+
avgLatencyMs: op.avgLatencyMs,
|
|
397
|
+
models: op.models.map((m) => m.modelId)
|
|
398
|
+
}));
|
|
399
|
+
this._operatorsCachedAt = now;
|
|
400
|
+
this.privateRouter.setOperators(this._cachedOperators);
|
|
401
|
+
}
|
|
140
402
|
/** Track cost after a response, using actual pricing from response headers when available */
|
|
141
403
|
trackCost(completion, res) {
|
|
142
404
|
this._requestCount++;
|
|
@@ -156,36 +418,154 @@ var TCloudClient = class {
|
|
|
156
418
|
}
|
|
157
419
|
}
|
|
158
420
|
}
|
|
159
|
-
/**
|
|
160
|
-
|
|
421
|
+
/**
|
|
422
|
+
* Core fetch with retry + timeout. All helpers build on this.
|
|
423
|
+
* Retries on retryable status codes with exponential backoff + jitter.
|
|
424
|
+
*/
|
|
425
|
+
async _doFetch(url, init, streaming) {
|
|
426
|
+
const retry = this.retryConfig;
|
|
427
|
+
const maxAttempts = retry ? retry.maxRetries + 1 : 1;
|
|
428
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
429
|
+
const controller = new AbortController();
|
|
430
|
+
let timer;
|
|
431
|
+
if (this.timeoutMs > 0 && !streaming) {
|
|
432
|
+
timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
433
|
+
}
|
|
434
|
+
try {
|
|
435
|
+
const res = await proxiedFetch(this.privacy, url, {
|
|
436
|
+
...init,
|
|
437
|
+
signal: controller.signal
|
|
438
|
+
}, streaming);
|
|
439
|
+
if (res.ok) return res;
|
|
440
|
+
if (retry && attempt < retry.maxRetries && retry.retryableStatuses.includes(res.status)) {
|
|
441
|
+
const backoff = Math.min(
|
|
442
|
+
retry.initialBackoffMs * Math.pow(retry.multiplier, attempt),
|
|
443
|
+
retry.maxBackoffMs
|
|
444
|
+
);
|
|
445
|
+
const jitter = backoff * 0.5 * Math.random();
|
|
446
|
+
await new Promise((r) => setTimeout(r, backoff + jitter));
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
450
|
+
throw new TCloudError(res.status, err.error?.message || err.error || err.message || res.statusText);
|
|
451
|
+
} catch (e) {
|
|
452
|
+
if (e instanceof TCloudError) throw e;
|
|
453
|
+
if (retry && attempt < retry.maxRetries) {
|
|
454
|
+
const backoff = Math.min(
|
|
455
|
+
retry.initialBackoffMs * Math.pow(retry.multiplier, attempt),
|
|
456
|
+
retry.maxBackoffMs
|
|
457
|
+
);
|
|
458
|
+
await new Promise((r) => setTimeout(r, backoff));
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
if (e?.name === "AbortError") {
|
|
462
|
+
throw new TCloudError(408, `Request timed out after ${this.timeoutMs}ms`);
|
|
463
|
+
}
|
|
464
|
+
throw new TCloudError(0, e?.message || "Network error");
|
|
465
|
+
} finally {
|
|
466
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
throw new TCloudError(0, "Retry loop exhausted");
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Shared request helper for billable JSON API calls.
|
|
473
|
+
* Enforces: checkLimits → fetch with retry/timeout → error parsing → requestCount.
|
|
474
|
+
*/
|
|
475
|
+
async _request(url, init = {}) {
|
|
476
|
+
this.checkLimits();
|
|
477
|
+
const res = await this._doFetch(url, { headers: this.headers, ...init }, false);
|
|
478
|
+
this._requestCount++;
|
|
479
|
+
return res.json();
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Shared request helper for read-only/non-billable JSON API calls.
|
|
483
|
+
* No limits check, no request counting.
|
|
484
|
+
*/
|
|
485
|
+
async _fetch(url, init = {}) {
|
|
486
|
+
const res = await this._doFetch(url, { headers: this.headers, ...init }, false);
|
|
487
|
+
return res.json();
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Shared request helper for billable calls that return non-JSON (e.g. ArrayBuffer).
|
|
491
|
+
*/
|
|
492
|
+
async _requestRaw(url, init = {}) {
|
|
161
493
|
this.checkLimits();
|
|
494
|
+
const res = await this._doFetch(url, { headers: this.headers, ...init }, false);
|
|
495
|
+
this._requestCount++;
|
|
496
|
+
return res;
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Prepare headers for chat requests — operator routing + SpendAuth +
|
|
500
|
+
* bridge short-circuit headers when `options.bridge` is set.
|
|
501
|
+
* Shared between chat() and chatStream() to eliminate duplication.
|
|
502
|
+
*/
|
|
503
|
+
async _prepareChatRequest(model, bridge) {
|
|
162
504
|
const headers = { ...this.headers };
|
|
163
505
|
if (this.spendAuthFn) {
|
|
164
506
|
const auth2 = await this.spendAuthFn();
|
|
165
507
|
headers["X-Payment-Signature"] = JSON.stringify(auth2);
|
|
166
508
|
delete headers["Authorization"];
|
|
167
509
|
}
|
|
168
|
-
|
|
510
|
+
let baseURL = this.baseURL;
|
|
511
|
+
if (this.privateRouter) {
|
|
512
|
+
await this.ensureRouterOperators();
|
|
513
|
+
const operator = this.privateRouter.selectOperator(model);
|
|
514
|
+
if (operator) {
|
|
515
|
+
baseURL = operator.endpointUrl.replace(/\/$/, "");
|
|
516
|
+
headers["X-Tangle-Operator"] = operator.slug;
|
|
517
|
+
delete headers["Authorization"];
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
if (bridge) {
|
|
521
|
+
headers["X-Bridge-Unlock"] = bridge.unlock;
|
|
522
|
+
if (bridge.resume) headers["X-Resume"] = bridge.resume;
|
|
523
|
+
if (bridge.bridgeUrl) headers["X-Bridge-Url"] = bridge.bridgeUrl;
|
|
524
|
+
if (bridge.bridgeBearer) headers["X-Bridge-Bearer"] = bridge.bridgeBearer;
|
|
525
|
+
}
|
|
526
|
+
return { headers, baseURL };
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Resolve the effective model string. When a bridge is set, rewrite to
|
|
530
|
+
* `bridge/<harness>/<model>` (or `bridge/<harness>` if no model).
|
|
531
|
+
*/
|
|
532
|
+
_effectiveModel(options) {
|
|
533
|
+
if (options.bridge) {
|
|
534
|
+
return options.bridge.model ? `bridge/${options.bridge.harness}/${options.bridge.model}` : `bridge/${options.bridge.harness}`;
|
|
535
|
+
}
|
|
536
|
+
return options.model || this.model;
|
|
537
|
+
}
|
|
538
|
+
/** Build the chat completions request body */
|
|
539
|
+
_chatBody(options, stream) {
|
|
540
|
+
return JSON.stringify({
|
|
541
|
+
model: this._effectiveModel(options),
|
|
542
|
+
messages: options.messages,
|
|
543
|
+
temperature: options.temperature,
|
|
544
|
+
max_tokens: options.maxTokens,
|
|
545
|
+
stream,
|
|
546
|
+
stop: options.stop,
|
|
547
|
+
top_p: options.topP,
|
|
548
|
+
frequency_penalty: options.frequencyPenalty,
|
|
549
|
+
presence_penalty: options.presencePenalty,
|
|
550
|
+
response_format: options.responseFormat,
|
|
551
|
+
tools: options.tools,
|
|
552
|
+
tool_choice: options.toolChoice,
|
|
553
|
+
...options.gateway ? { gateway: options.gateway } : {},
|
|
554
|
+
...options.providerOptions
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
/** Chat completion (non-streaming) */
|
|
558
|
+
async chat(options) {
|
|
559
|
+
this.checkLimits();
|
|
560
|
+
const { headers, baseURL } = await this._prepareChatRequest(
|
|
561
|
+
this._effectiveModel(options),
|
|
562
|
+
options.bridge
|
|
563
|
+
);
|
|
564
|
+
const res = await this._doFetch(`${baseURL}/chat/completions`, {
|
|
169
565
|
method: "POST",
|
|
170
566
|
headers,
|
|
171
|
-
body:
|
|
172
|
-
model: options.model || this.model,
|
|
173
|
-
messages: options.messages,
|
|
174
|
-
temperature: options.temperature,
|
|
175
|
-
max_tokens: options.maxTokens,
|
|
176
|
-
stream: false,
|
|
177
|
-
stop: options.stop,
|
|
178
|
-
top_p: options.topP,
|
|
179
|
-
frequency_penalty: options.frequencyPenalty,
|
|
180
|
-
presence_penalty: options.presencePenalty,
|
|
181
|
-
response_format: options.responseFormat,
|
|
182
|
-
tools: options.tools
|
|
183
|
-
})
|
|
567
|
+
body: this._chatBody(options, false)
|
|
184
568
|
}, false);
|
|
185
|
-
if (!res.ok) {
|
|
186
|
-
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
187
|
-
throw new TCloudError(res.status, err.error?.message || err.error || err.message || res.statusText);
|
|
188
|
-
}
|
|
189
569
|
const completion = await res.json();
|
|
190
570
|
this.trackCost(completion, res);
|
|
191
571
|
return completion;
|
|
@@ -193,29 +573,16 @@ var TCloudClient = class {
|
|
|
193
573
|
/** Chat completion (streaming) — returns an async iterator of chunks */
|
|
194
574
|
async *chatStream(options) {
|
|
195
575
|
this.checkLimits();
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
}
|
|
202
|
-
const res = await proxiedFetch(this.privacy, `${this.baseURL}/chat/completions`, {
|
|
576
|
+
this._requestCount++;
|
|
577
|
+
const { headers, baseURL } = await this._prepareChatRequest(
|
|
578
|
+
this._effectiveModel(options),
|
|
579
|
+
options.bridge
|
|
580
|
+
);
|
|
581
|
+
const res = await this._doFetch(`${baseURL}/chat/completions`, {
|
|
203
582
|
method: "POST",
|
|
204
583
|
headers,
|
|
205
|
-
body:
|
|
206
|
-
model: options.model || this.model,
|
|
207
|
-
messages: options.messages,
|
|
208
|
-
temperature: options.temperature,
|
|
209
|
-
max_tokens: options.maxTokens,
|
|
210
|
-
stream: true,
|
|
211
|
-
stop: options.stop,
|
|
212
|
-
top_p: options.topP
|
|
213
|
-
})
|
|
584
|
+
body: this._chatBody(options, true)
|
|
214
585
|
}, true);
|
|
215
|
-
if (!res.ok) {
|
|
216
|
-
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
217
|
-
throw new TCloudError(res.status, err.error || err.message || res.statusText);
|
|
218
|
-
}
|
|
219
586
|
const reader = res.body.getReader();
|
|
220
587
|
const decoder = new TextDecoder();
|
|
221
588
|
let buf = "";
|
|
@@ -223,13 +590,13 @@ var TCloudClient = class {
|
|
|
223
590
|
const { done, value } = await reader.read();
|
|
224
591
|
if (done) break;
|
|
225
592
|
buf += decoder.decode(value, { stream: true });
|
|
593
|
+
if (buf.length > 1048576) throw new TCloudError(502, "SSE buffer overflow \u2014 server sent >1MB without newline");
|
|
226
594
|
const lines = buf.split("\n");
|
|
227
595
|
buf = lines.pop() || "";
|
|
228
596
|
for (const line of lines) {
|
|
229
597
|
if (!line.startsWith("data: ")) continue;
|
|
230
598
|
const data = line.slice(6).trim();
|
|
231
599
|
if (data === "[DONE]") {
|
|
232
|
-
this._requestCount++;
|
|
233
600
|
return;
|
|
234
601
|
}
|
|
235
602
|
try {
|
|
@@ -239,6 +606,24 @@ var TCloudClient = class {
|
|
|
239
606
|
}
|
|
240
607
|
}
|
|
241
608
|
}
|
|
609
|
+
/**
|
|
610
|
+
* Bridge — scoped helper for a subscription-backed CLI harness behind
|
|
611
|
+
* the Tangle Router's cli-bridge. Returns a mini-client bound to
|
|
612
|
+
* (harness, unlock, resume) so you don't thread those through every
|
|
613
|
+
* call.
|
|
614
|
+
*
|
|
615
|
+
* ```ts
|
|
616
|
+
* const kimi = tcloud.bridge({ harness: 'kimi', model: 'kimi-for-coding', unlock: UNLOCK, resume: 'pr-42' })
|
|
617
|
+
* await kimi.ask('review this diff…')
|
|
618
|
+
* for await (const chunk of kimi.stream('continue…')) process.stdout.write(chunk)
|
|
619
|
+
* ```
|
|
620
|
+
*
|
|
621
|
+
* Sessions persist across process restarts — use the same `resume` id
|
|
622
|
+
* to land on the same CLI conversation (context intact, no replay tax).
|
|
623
|
+
*/
|
|
624
|
+
bridge(cfg) {
|
|
625
|
+
return new BridgeSession(this, cfg);
|
|
626
|
+
}
|
|
242
627
|
/** Convenience: send a single message and get the text response */
|
|
243
628
|
async ask(message, modelOrOptions) {
|
|
244
629
|
const options = typeof modelOrOptions === "string" ? { model: modelOrOptions } : modelOrOptions;
|
|
@@ -269,85 +654,112 @@ var TCloudClient = class {
|
|
|
269
654
|
}
|
|
270
655
|
/** List available models */
|
|
271
656
|
async models() {
|
|
272
|
-
const
|
|
273
|
-
if (!res.ok) throw new TCloudError(res.status, "Failed to fetch models");
|
|
274
|
-
const data = await res.json();
|
|
657
|
+
const data = await this._fetch(`${this.baseURL}/models`);
|
|
275
658
|
return data.data || [];
|
|
276
659
|
}
|
|
277
660
|
/** List active operators */
|
|
278
661
|
async operators() {
|
|
279
662
|
const apiRoot = this.baseURL.replace(/\/v1$/, "");
|
|
280
|
-
|
|
281
|
-
if (!res.ok) throw new TCloudError(res.status, "Failed to fetch operators");
|
|
282
|
-
return res.json();
|
|
663
|
+
return this._fetch(`${apiRoot}/api/operators`);
|
|
283
664
|
}
|
|
665
|
+
// ── Billing (via id.tangle.tools) ──
|
|
284
666
|
/** Get credit balance */
|
|
285
667
|
async credits() {
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
if (!res.ok) throw new TCloudError(res.status, "Failed to fetch credits");
|
|
289
|
-
return res.json();
|
|
668
|
+
const { data } = await this._fetch(`${this.platformURL}/v1/billing/balance`);
|
|
669
|
+
return data;
|
|
290
670
|
}
|
|
291
|
-
/** Add credits */
|
|
671
|
+
/** Add credits via Stripe checkout. Returns the checkout URL. */
|
|
292
672
|
async addCredits(amount) {
|
|
293
|
-
const
|
|
294
|
-
const res = await proxiedFetch(this.privacy, `${apiRoot}/api/billing`, {
|
|
673
|
+
const { data } = await this._fetch(`${this.platformURL}/v1/billing/topup`, {
|
|
295
674
|
method: "POST",
|
|
296
|
-
headers: this.headers,
|
|
297
675
|
body: JSON.stringify({ amount })
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
return res.json();
|
|
676
|
+
});
|
|
677
|
+
return data;
|
|
301
678
|
}
|
|
302
|
-
/**
|
|
303
|
-
async
|
|
304
|
-
const
|
|
305
|
-
|
|
679
|
+
/** Get transaction history */
|
|
680
|
+
async transactions(limit = 50) {
|
|
681
|
+
const { data } = await this._fetch(`${this.platformURL}/v1/billing/transactions?limit=${limit}`);
|
|
682
|
+
return data;
|
|
683
|
+
}
|
|
684
|
+
// ── API Keys (via id.tangle.tools) ──
|
|
685
|
+
/**
|
|
686
|
+
* Create a new API key.
|
|
687
|
+
* When called with an API key (not session), the new key is automatically
|
|
688
|
+
* a child of the calling key — enabling hierarchical key delegation.
|
|
689
|
+
*
|
|
690
|
+
* Pass `parentKeyId` explicitly to create a child of a specific key.
|
|
691
|
+
* Child keys inherit the parent's product scope, allowedModels, and rpmLimit
|
|
692
|
+
* if not specified. Budget cannot exceed the parent's remaining budget.
|
|
693
|
+
*/
|
|
694
|
+
async createKey(opts) {
|
|
695
|
+
const { data } = await this._fetch(`${this.platformURL}/v1/keys`, {
|
|
306
696
|
method: "POST",
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
if (!res.ok) throw new TCloudError(res.status, "Failed to create API key");
|
|
311
|
-
return res.json();
|
|
697
|
+
body: JSON.stringify(opts)
|
|
698
|
+
});
|
|
699
|
+
return data;
|
|
312
700
|
}
|
|
313
|
-
/**
|
|
314
|
-
async
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
701
|
+
/** Get a single API key by ID */
|
|
702
|
+
async getKey(id) {
|
|
703
|
+
const { data } = await this._fetch(`${this.platformURL}/v1/keys/${id}`);
|
|
704
|
+
return data;
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* List API keys.
|
|
708
|
+
* Pass `children: true` to list child keys of the calling API key.
|
|
709
|
+
*/
|
|
710
|
+
async keys(opts) {
|
|
711
|
+
const q = opts?.children ? "?children=true" : "";
|
|
712
|
+
const { data } = await this._fetch(`${this.platformURL}/v1/keys${q}`);
|
|
713
|
+
return data;
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Update an API key's limits.
|
|
717
|
+
* Can adjust budget, allowedModels, rpmLimit, expiresAt, and name.
|
|
718
|
+
*/
|
|
719
|
+
async updateKey(id, updates) {
|
|
720
|
+
const { data } = await this._fetch(`${this.platformURL}/v1/keys/${id}`, {
|
|
721
|
+
method: "PATCH",
|
|
722
|
+
body: JSON.stringify(updates)
|
|
723
|
+
});
|
|
724
|
+
return data;
|
|
319
725
|
}
|
|
320
|
-
/** Revoke an API key */
|
|
726
|
+
/** Revoke an API key. If the key has children, they are also revoked recursively. */
|
|
321
727
|
async revokeKey(id) {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
}
|
|
327
|
-
|
|
728
|
+
await this._fetch(`${this.platformURL}/v1/keys/${id}`, { method: "DELETE" });
|
|
729
|
+
}
|
|
730
|
+
/** Rotate an API key — creates new key with same config, revokes old */
|
|
731
|
+
async rotateKey(id) {
|
|
732
|
+
const { data } = await this._fetch(`${this.platformURL}/v1/keys/${id}/rotate`, { method: "POST" });
|
|
733
|
+
return data;
|
|
734
|
+
}
|
|
735
|
+
// ── Projects (via id.tangle.tools) ──
|
|
736
|
+
/** Create a project for usage attribution */
|
|
737
|
+
async createProject(name, product) {
|
|
738
|
+
const { data } = await this._fetch(`${this.platformURL}/v1/projects`, {
|
|
739
|
+
method: "POST",
|
|
740
|
+
body: JSON.stringify({ name, product })
|
|
741
|
+
});
|
|
742
|
+
return data;
|
|
743
|
+
}
|
|
744
|
+
/** List projects */
|
|
745
|
+
async projects() {
|
|
746
|
+
const { data } = await this._fetch(`${this.platformURL}/v1/projects`);
|
|
747
|
+
return data;
|
|
328
748
|
}
|
|
329
749
|
/** Generate embeddings */
|
|
330
750
|
async embeddings(options) {
|
|
331
|
-
|
|
751
|
+
return this._request(`${this.baseURL}/embeddings`, {
|
|
332
752
|
method: "POST",
|
|
333
|
-
headers: this.headers,
|
|
334
753
|
body: JSON.stringify({
|
|
335
754
|
model: options.model || "text-embedding-3-small",
|
|
336
755
|
input: options.input
|
|
337
756
|
})
|
|
338
|
-
}
|
|
339
|
-
if (!res.ok) {
|
|
340
|
-
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
341
|
-
throw new TCloudError(res.status, err.error?.message || err.error || res.statusText);
|
|
342
|
-
}
|
|
343
|
-
this._requestCount++;
|
|
344
|
-
return res.json();
|
|
757
|
+
});
|
|
345
758
|
}
|
|
346
759
|
/** Generate images */
|
|
347
760
|
async imageGenerate(options) {
|
|
348
|
-
|
|
761
|
+
return this._request(`${this.baseURL}/images/generations`, {
|
|
349
762
|
method: "POST",
|
|
350
|
-
headers: this.headers,
|
|
351
763
|
body: JSON.stringify({
|
|
352
764
|
model: options.model || "dall-e-3",
|
|
353
765
|
prompt: options.prompt,
|
|
@@ -356,56 +768,36 @@ var TCloudClient = class {
|
|
|
356
768
|
quality: options.quality,
|
|
357
769
|
response_format: options.response_format
|
|
358
770
|
})
|
|
359
|
-
}
|
|
360
|
-
if (!res.ok) {
|
|
361
|
-
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
362
|
-
throw new TCloudError(res.status, err.error?.message || err.error || res.statusText);
|
|
363
|
-
}
|
|
364
|
-
this._requestCount++;
|
|
365
|
-
return res.json();
|
|
771
|
+
});
|
|
366
772
|
}
|
|
367
773
|
/** Rerank documents by relevance to a query */
|
|
368
774
|
async rerank(options) {
|
|
369
|
-
|
|
775
|
+
return this._request(`${this.baseURL}/rerank`, {
|
|
370
776
|
method: "POST",
|
|
371
|
-
headers: this.headers,
|
|
372
777
|
body: JSON.stringify({
|
|
373
778
|
model: options.model || "rerank-english-v3.0",
|
|
374
779
|
query: options.query,
|
|
375
780
|
documents: options.documents,
|
|
376
781
|
top_n: options.top_n
|
|
377
782
|
})
|
|
378
|
-
}
|
|
379
|
-
if (!res.ok) {
|
|
380
|
-
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
381
|
-
throw new TCloudError(res.status, err.error?.message || err.error || res.statusText);
|
|
382
|
-
}
|
|
383
|
-
this._requestCount++;
|
|
384
|
-
return res.json();
|
|
783
|
+
});
|
|
385
784
|
}
|
|
386
785
|
/** Text-to-speech */
|
|
387
786
|
async speech(options) {
|
|
388
|
-
const res = await
|
|
787
|
+
const res = await this._requestRaw(`${this.baseURL}/audio/speech`, {
|
|
389
788
|
method: "POST",
|
|
390
|
-
headers: this.headers,
|
|
391
789
|
body: JSON.stringify({
|
|
392
790
|
model: options.model || "tts-1",
|
|
393
791
|
input: options.input,
|
|
394
792
|
voice: options.voice || "alloy"
|
|
395
793
|
})
|
|
396
|
-
}
|
|
397
|
-
if (!res.ok) {
|
|
398
|
-
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
399
|
-
throw new TCloudError(res.status, err.error?.message || err.error || res.statusText);
|
|
400
|
-
}
|
|
401
|
-
this._requestCount++;
|
|
794
|
+
});
|
|
402
795
|
return res.arrayBuffer();
|
|
403
796
|
}
|
|
404
797
|
/** Legacy completions endpoint */
|
|
405
798
|
async completions(options) {
|
|
406
|
-
|
|
799
|
+
return this._request(`${this.baseURL}/completions`, {
|
|
407
800
|
method: "POST",
|
|
408
|
-
headers: this.headers,
|
|
409
801
|
body: JSON.stringify({
|
|
410
802
|
model: options.model || this.model,
|
|
411
803
|
prompt: options.prompt,
|
|
@@ -414,13 +806,7 @@ var TCloudClient = class {
|
|
|
414
806
|
stop: options.stop,
|
|
415
807
|
top_p: options.topP
|
|
416
808
|
})
|
|
417
|
-
}
|
|
418
|
-
if (!res.ok) {
|
|
419
|
-
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
420
|
-
throw new TCloudError(res.status, err.error?.message || err.error || res.statusText);
|
|
421
|
-
}
|
|
422
|
-
this._requestCount++;
|
|
423
|
-
return res.json();
|
|
809
|
+
});
|
|
424
810
|
}
|
|
425
811
|
/** Audio transcription (speech-to-text) */
|
|
426
812
|
async transcribe(file, options) {
|
|
@@ -431,6 +817,7 @@ var TCloudClient = class {
|
|
|
431
817
|
if (options?.prompt) formData.append("prompt", options.prompt);
|
|
432
818
|
const headers = { ...this.headers };
|
|
433
819
|
delete headers["Content-Type"];
|
|
820
|
+
this.checkLimits();
|
|
434
821
|
const res = await proxiedFetch(this.privacy, `${this.baseURL}/audio/transcriptions`, {
|
|
435
822
|
method: "POST",
|
|
436
823
|
headers,
|
|
@@ -438,75 +825,181 @@ var TCloudClient = class {
|
|
|
438
825
|
}, false);
|
|
439
826
|
if (!res.ok) {
|
|
440
827
|
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
441
|
-
throw new TCloudError(res.status, err.error?.message || err.error || res.statusText);
|
|
828
|
+
throw new TCloudError(res.status, err.error?.message || err.error || err.message || res.statusText);
|
|
442
829
|
}
|
|
443
830
|
this._requestCount++;
|
|
444
831
|
return res.json();
|
|
445
832
|
}
|
|
446
833
|
/** Create a fine-tuning job */
|
|
447
834
|
async fineTuneCreate(options) {
|
|
448
|
-
|
|
835
|
+
return this._request(`${this.baseURL}/fine_tuning/jobs`, {
|
|
449
836
|
method: "POST",
|
|
450
|
-
headers: this.headers,
|
|
451
837
|
body: JSON.stringify(options)
|
|
452
|
-
}
|
|
453
|
-
if (!res.ok) {
|
|
454
|
-
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
455
|
-
throw new TCloudError(res.status, err.error?.message || err.error || res.statusText);
|
|
456
|
-
}
|
|
457
|
-
this._requestCount++;
|
|
458
|
-
return res.json();
|
|
838
|
+
});
|
|
459
839
|
}
|
|
460
840
|
/** List fine-tuning jobs */
|
|
461
841
|
async fineTuneList() {
|
|
462
|
-
|
|
463
|
-
headers: this.headers
|
|
464
|
-
}, false);
|
|
465
|
-
if (!res.ok) throw new TCloudError(res.status, "Failed to fetch fine-tuning jobs");
|
|
466
|
-
return res.json();
|
|
842
|
+
return this._fetch(`${this.baseURL}/fine_tuning/jobs`);
|
|
467
843
|
}
|
|
468
844
|
/** Submit a batch of chat requests */
|
|
469
845
|
async batch(requests) {
|
|
470
|
-
|
|
846
|
+
return this._request(`${this.baseURL}/batch`, {
|
|
471
847
|
method: "POST",
|
|
472
|
-
headers: this.headers,
|
|
473
848
|
body: JSON.stringify({ requests })
|
|
474
|
-
}
|
|
475
|
-
if (!res.ok) {
|
|
476
|
-
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
477
|
-
throw new TCloudError(res.status, err.error?.message || err.error || res.statusText);
|
|
478
|
-
}
|
|
479
|
-
return res.json();
|
|
849
|
+
});
|
|
480
850
|
}
|
|
481
851
|
/** Get batch job status */
|
|
482
852
|
async batchStatus(jobId) {
|
|
483
|
-
|
|
484
|
-
headers: this.headers
|
|
485
|
-
}, false);
|
|
486
|
-
if (!res.ok) throw new TCloudError(res.status, "Failed to fetch batch status");
|
|
487
|
-
return res.json();
|
|
853
|
+
return this._fetch(`${this.baseURL}/batch?id=${jobId}`);
|
|
488
854
|
}
|
|
489
855
|
/** Generate video */
|
|
490
856
|
async videoGenerate(options) {
|
|
491
|
-
|
|
857
|
+
return this._request(`${this.baseURL}/video/generate`, {
|
|
492
858
|
method: "POST",
|
|
493
|
-
headers: this.headers,
|
|
494
859
|
body: JSON.stringify(options)
|
|
495
|
-
}
|
|
496
|
-
if (!res.ok) {
|
|
497
|
-
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
498
|
-
throw new TCloudError(res.status, err.error?.message || err.error || res.statusText);
|
|
499
|
-
}
|
|
500
|
-
this._requestCount++;
|
|
501
|
-
return res.json();
|
|
860
|
+
});
|
|
502
861
|
}
|
|
503
862
|
/** Get video generation status */
|
|
504
863
|
async videoStatus(id) {
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
864
|
+
return this._fetch(`${this.baseURL}/video?id=${id}`);
|
|
865
|
+
}
|
|
866
|
+
/** Generate an avatar video (lip-synced talking head from audio + face image).
|
|
867
|
+
* Returns 202 with a job_id for async polling via avatarJobStatus(). */
|
|
868
|
+
async avatarGenerate(options) {
|
|
869
|
+
return this._request(`${this.baseURL}/avatar/generate`, {
|
|
870
|
+
method: "POST",
|
|
871
|
+
body: JSON.stringify(options)
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
/** Poll an avatar generation job by ID. */
|
|
875
|
+
async avatarJobStatus(jobId) {
|
|
876
|
+
return this._fetch(`${this.baseURL}/avatar/jobs/${jobId}`);
|
|
877
|
+
}
|
|
878
|
+
/** Poll an avatar job until it reaches a terminal state (completed/failed).
|
|
879
|
+
* Returns the final job status. Throws on failure. */
|
|
880
|
+
async pollAvatarJob(jobId, options) {
|
|
881
|
+
const interval = options?.intervalMs ?? 5e3;
|
|
882
|
+
const timeout = options?.timeoutMs ?? 3e5;
|
|
883
|
+
const deadline = Date.now() + timeout;
|
|
884
|
+
while (Date.now() < deadline) {
|
|
885
|
+
const job = await this.avatarJobStatus(jobId);
|
|
886
|
+
if (job.status === "completed") return job;
|
|
887
|
+
if (job.status === "failed") {
|
|
888
|
+
throw new TCloudError(500, job.error || `Avatar job ${jobId} failed`);
|
|
889
|
+
}
|
|
890
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
891
|
+
}
|
|
892
|
+
throw new TCloudError(408, `Avatar job ${jobId} timed out after ${timeout}ms`);
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* Watch an async job via SSE until it reaches a terminal state.
|
|
896
|
+
* Works with avatar, video, and training blueprint operators.
|
|
897
|
+
*
|
|
898
|
+
* @param jobId - The job ID returned by the creation endpoint
|
|
899
|
+
* @param options - Optional: operatorUrl override, onEvent callback
|
|
900
|
+
* @returns The final JobEvent (completed/failed/cancelled)
|
|
901
|
+
*/
|
|
902
|
+
async watchJob(jobId, options) {
|
|
903
|
+
const base = options?.operatorUrl?.replace(/\/$/, "") || this.baseURL;
|
|
904
|
+
const url = `${base}/v1/jobs/${encodeURIComponent(jobId)}/events`;
|
|
905
|
+
const timeout = options?.timeout ?? 3e5;
|
|
906
|
+
const controller = new AbortController();
|
|
907
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
908
|
+
try {
|
|
909
|
+
const watchHeaders = {
|
|
910
|
+
...this.headers,
|
|
911
|
+
Accept: "text/event-stream"
|
|
912
|
+
};
|
|
913
|
+
if (options?.operatorUrl) {
|
|
914
|
+
delete watchHeaders["Authorization"];
|
|
915
|
+
}
|
|
916
|
+
if (options?.sseToken) {
|
|
917
|
+
watchHeaders["Authorization"] = `Bearer ${options.sseToken}`;
|
|
918
|
+
}
|
|
919
|
+
const res = await proxiedFetch(this.privacy, url, {
|
|
920
|
+
headers: watchHeaders,
|
|
921
|
+
signal: controller.signal
|
|
922
|
+
}, true);
|
|
923
|
+
if (!res.ok) {
|
|
924
|
+
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
925
|
+
throw new TCloudError(res.status, err.error?.message || err.error || res.statusText);
|
|
926
|
+
}
|
|
927
|
+
const reader = res.body.getReader();
|
|
928
|
+
const decoder = new TextDecoder();
|
|
929
|
+
let buf = "";
|
|
930
|
+
const terminalStatuses = /* @__PURE__ */ new Set(["completed", "failed", "cancelled"]);
|
|
931
|
+
while (true) {
|
|
932
|
+
const { done, value } = await reader.read();
|
|
933
|
+
if (done) {
|
|
934
|
+
throw new TCloudError(502, `SSE stream ended without terminal event for job ${jobId}`);
|
|
935
|
+
}
|
|
936
|
+
buf += decoder.decode(value, { stream: true });
|
|
937
|
+
if (buf.length > 1048576) throw new TCloudError(502, "SSE buffer overflow \u2014 server sent >1MB without newline");
|
|
938
|
+
const lines = buf.split("\n");
|
|
939
|
+
buf = lines.pop() || "";
|
|
940
|
+
for (const line of lines) {
|
|
941
|
+
if (!line.startsWith("data: ")) continue;
|
|
942
|
+
const data = line.slice(6).trim();
|
|
943
|
+
if (!data || data === "[DONE]") continue;
|
|
944
|
+
let event;
|
|
945
|
+
try {
|
|
946
|
+
event = JSON.parse(data);
|
|
947
|
+
} catch {
|
|
948
|
+
continue;
|
|
949
|
+
}
|
|
950
|
+
try {
|
|
951
|
+
options?.onEvent?.(event);
|
|
952
|
+
} catch (cbErr) {
|
|
953
|
+
console.error("watchJob onEvent callback error:", cbErr);
|
|
954
|
+
}
|
|
955
|
+
if (terminalStatuses.has(event.status)) {
|
|
956
|
+
return event;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
} catch (err) {
|
|
961
|
+
if (err?.name === "AbortError") {
|
|
962
|
+
throw new TCloudError(408, `Job ${jobId} timed out after ${timeout}ms`);
|
|
963
|
+
}
|
|
964
|
+
throw err;
|
|
965
|
+
} finally {
|
|
966
|
+
clearTimeout(timer);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
// ---------------------------------------------------------------------------
|
|
970
|
+
// Vector Store (requires operator routing — X-Tangle-Service/Blueprint/Operator)
|
|
971
|
+
// ---------------------------------------------------------------------------
|
|
972
|
+
/** Create a vector collection on the operator's vector store */
|
|
973
|
+
async createCollection(options) {
|
|
974
|
+
return this._request(`${this.baseURL}/collections`, {
|
|
975
|
+
method: "POST",
|
|
976
|
+
body: JSON.stringify(options)
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
/** List collections on the operator's vector store */
|
|
980
|
+
async listCollections() {
|
|
981
|
+
return this._fetch(`${this.baseURL}/collections`);
|
|
982
|
+
}
|
|
983
|
+
/** Upsert vectors into a collection */
|
|
984
|
+
async upsertVectors(collection, vectors) {
|
|
985
|
+
return this._request(`${this.baseURL}/collections/${encodeURIComponent(collection)}/upsert`, {
|
|
986
|
+
method: "POST",
|
|
987
|
+
body: JSON.stringify({ vectors })
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
/** Similarity search in a collection */
|
|
991
|
+
async queryVectors(collection, options) {
|
|
992
|
+
return this._request(`${this.baseURL}/collections/${encodeURIComponent(collection)}/query`, {
|
|
993
|
+
method: "POST",
|
|
994
|
+
body: JSON.stringify(options)
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
/** RAG query — embed text + search collection in one call */
|
|
998
|
+
async ragQuery(options) {
|
|
999
|
+
return this._request(`${this.baseURL}/rag`, {
|
|
1000
|
+
method: "POST",
|
|
1001
|
+
body: JSON.stringify(options)
|
|
1002
|
+
});
|
|
510
1003
|
}
|
|
511
1004
|
/** Search models by name, provider, or capability */
|
|
512
1005
|
async searchModels(query) {
|
|
@@ -525,7 +1018,176 @@ var TCloudClient = class {
|
|
|
525
1018
|
const outputCost = options.outputTokens * parseFloat(model.pricing.completion);
|
|
526
1019
|
return { inputCost, outputCost, total: inputCost + outputCost };
|
|
527
1020
|
}
|
|
1021
|
+
/**
|
|
1022
|
+
* Get a pricing spectrum across resource tiers for a model.
|
|
1023
|
+
*
|
|
1024
|
+
* Uses REAL per-operator pricing from `operator.models[].inputPrice`.
|
|
1025
|
+
* Each tier filters operators by GPU count and TEE capability, then
|
|
1026
|
+
* reports the cheapest and most expensive operator for that config.
|
|
1027
|
+
*
|
|
1028
|
+
* @param options.model - Model ID to price (falls back to client default)
|
|
1029
|
+
* @param options.tiers - Number of tiers (1-7, default 5)
|
|
1030
|
+
*/
|
|
1031
|
+
async pricingSpectrum(options) {
|
|
1032
|
+
const requestedTiers = Math.max(1, Math.min(options.tiers ?? 5, ALL_TIERS.length));
|
|
1033
|
+
const modelId = options.model || this.model;
|
|
1034
|
+
const selected = selectTiers(ALL_TIERS, requestedTiers);
|
|
1035
|
+
const operatorData = await this.operators();
|
|
1036
|
+
const allOperators = operatorData.operators || [];
|
|
1037
|
+
return selected.map((tier) => {
|
|
1038
|
+
const matching = allOperators.filter((op) => {
|
|
1039
|
+
if (tier.gpu > 0 && (op.gpuCount ?? 0) < tier.gpu) return false;
|
|
1040
|
+
if (tier.tee && !op.teeAttested) return false;
|
|
1041
|
+
return true;
|
|
1042
|
+
});
|
|
1043
|
+
const prices = matching.map((op) => op.models.find((m) => m.modelId === modelId)?.inputPrice).filter((p) => p != null && p > 0).sort((a, b) => a - b);
|
|
1044
|
+
const cheapestPrice = prices[0];
|
|
1045
|
+
const priciestPrice = prices.length > 1 ? prices[prices.length - 1] : void 0;
|
|
1046
|
+
return {
|
|
1047
|
+
tier: tier.name,
|
|
1048
|
+
config: tier,
|
|
1049
|
+
cheapestPrice,
|
|
1050
|
+
priciestPrice: priciestPrice !== cheapestPrice ? priciestPrice : void 0,
|
|
1051
|
+
cheapest: cheapestPrice != null ? formatPrice(cheapestPrice) : "no operators for this config",
|
|
1052
|
+
priciest: priciestPrice != null && priciestPrice !== cheapestPrice ? formatPrice(priciestPrice) : void 0,
|
|
1053
|
+
availableOperators: matching.length,
|
|
1054
|
+
operatorsWithModel: prices.length
|
|
1055
|
+
};
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
// ── Eval ──────────────────────────────────────────────────────────────
|
|
1059
|
+
get _apiRoot() {
|
|
1060
|
+
return this.baseURL.replace(/\/v1$/, "");
|
|
1061
|
+
}
|
|
1062
|
+
async eval(opts) {
|
|
1063
|
+
return this._request(`${this._apiRoot}/api/eval`, { method: "POST", body: JSON.stringify(opts) });
|
|
1064
|
+
}
|
|
1065
|
+
async createSuite(opts) {
|
|
1066
|
+
return this._request(`${this._apiRoot}/api/eval/suites`, { method: "POST", body: JSON.stringify(opts) });
|
|
1067
|
+
}
|
|
1068
|
+
async listSuites() {
|
|
1069
|
+
return this._fetch(`${this._apiRoot}/api/eval/suites`);
|
|
1070
|
+
}
|
|
1071
|
+
async runSuite(suiteId, opts) {
|
|
1072
|
+
return this._request(`${this._apiRoot}/api/eval/suites/${suiteId}/runs`, { method: "POST", body: JSON.stringify(opts || {}) });
|
|
1073
|
+
}
|
|
1074
|
+
async listRuns(suiteId) {
|
|
1075
|
+
return this._fetch(`${this._apiRoot}/api/eval/suites/${suiteId}/runs`);
|
|
1076
|
+
}
|
|
1077
|
+
async getRun(runId) {
|
|
1078
|
+
return this._fetch(`${this._apiRoot}/api/eval/runs/${runId}`);
|
|
1079
|
+
}
|
|
1080
|
+
async setBaseline(runId) {
|
|
1081
|
+
await this._request(`${this._apiRoot}/api/eval/runs/${runId}`, { method: "PATCH", body: JSON.stringify({ baseline: true }) });
|
|
1082
|
+
}
|
|
1083
|
+
// ── Sandbox ──────────────────────────────────────────────────────────
|
|
1084
|
+
async sandboxPricing(opts) {
|
|
1085
|
+
const p = new URLSearchParams();
|
|
1086
|
+
if (opts?.cpu) p.set("cpu", String(opts.cpu));
|
|
1087
|
+
if (opts?.ram) p.set("ram", String(opts.ram));
|
|
1088
|
+
if (opts?.disk) p.set("disk", String(opts.disk));
|
|
1089
|
+
return this._fetch(`${this._apiRoot}/api/sandbox/pricing?${p}`);
|
|
1090
|
+
}
|
|
1091
|
+
async sandboxStatus() {
|
|
1092
|
+
return this._fetch(`${this._apiRoot}/api/sandbox/link-key`);
|
|
1093
|
+
}
|
|
1094
|
+
async sandboxProvision() {
|
|
1095
|
+
return this._request(`${this._apiRoot}/api/sandbox/provision`, { method: "POST" });
|
|
1096
|
+
}
|
|
1097
|
+
async sandboxCreate(opts) {
|
|
1098
|
+
return this._request(`${this._apiRoot}/api/sandbox/sessions`, { method: "POST", body: JSON.stringify(opts) });
|
|
1099
|
+
}
|
|
1100
|
+
async sandboxList() {
|
|
1101
|
+
return this._fetch(`${this._apiRoot}/api/sandbox/sessions`);
|
|
1102
|
+
}
|
|
1103
|
+
async sandboxStats(sandboxId) {
|
|
1104
|
+
return this._fetch(`${this._apiRoot}/api/sandbox/stats/${sandboxId}`);
|
|
1105
|
+
}
|
|
1106
|
+
async sandboxDestroy(sessionId) {
|
|
1107
|
+
return this._request(`${this._apiRoot}/api/sandbox/sessions/${sessionId}`, { method: "DELETE" });
|
|
1108
|
+
}
|
|
1109
|
+
// ── User Info ────────────────────────────────────────────────────────
|
|
1110
|
+
async userInfo() {
|
|
1111
|
+
return this._fetch(`${this._apiRoot}/api/auth/userinfo`);
|
|
1112
|
+
}
|
|
1113
|
+
};
|
|
1114
|
+
var ALL_TIERS = [
|
|
1115
|
+
{ name: "cpu-only", cpu: 4, ramGb: 16, gpu: 0, tee: false },
|
|
1116
|
+
{ name: "gpu", cpu: 8, ramGb: 32, gpu: 1, tee: false },
|
|
1117
|
+
{ name: "gpu-tee", cpu: 8, ramGb: 32, gpu: 1, tee: true },
|
|
1118
|
+
{ name: "multi-gpu", cpu: 32, ramGb: 128, gpu: 2, tee: false },
|
|
1119
|
+
{ name: "multi-gpu-tee", cpu: 32, ramGb: 128, gpu: 2, tee: true },
|
|
1120
|
+
{ name: "max-gpu", cpu: 64, ramGb: 256, gpu: 4, tee: false },
|
|
1121
|
+
{ name: "max-gpu-tee", cpu: 64, ramGb: 256, gpu: 4, tee: true }
|
|
1122
|
+
];
|
|
1123
|
+
var BridgeSession = class _BridgeSession {
|
|
1124
|
+
constructor(client, cfg) {
|
|
1125
|
+
this.client = client;
|
|
1126
|
+
this.cfg = cfg;
|
|
1127
|
+
}
|
|
1128
|
+
/** Full chat completion (non-streaming). */
|
|
1129
|
+
async chat(options) {
|
|
1130
|
+
return this.client.chat({ ...options, bridge: this.cfg });
|
|
1131
|
+
}
|
|
1132
|
+
/** Stream OpenAI chat.completion.chunks. */
|
|
1133
|
+
chatStream(options) {
|
|
1134
|
+
return this.client.chatStream({ ...options, bridge: this.cfg });
|
|
1135
|
+
}
|
|
1136
|
+
/** One-shot: send a string, get the assistant text. */
|
|
1137
|
+
async ask(message, extra) {
|
|
1138
|
+
const completion = await this.chat({
|
|
1139
|
+
messages: [{ role: "user", content: message }],
|
|
1140
|
+
...extra
|
|
1141
|
+
});
|
|
1142
|
+
return completion.choices[0]?.message?.content || "";
|
|
1143
|
+
}
|
|
1144
|
+
/** One-shot: send a string, stream text deltas. */
|
|
1145
|
+
async *stream(message, extra) {
|
|
1146
|
+
for await (const chunk of this.chatStream({
|
|
1147
|
+
messages: [{ role: "user", content: message }],
|
|
1148
|
+
...extra
|
|
1149
|
+
})) {
|
|
1150
|
+
const content = chunk.choices?.[0]?.delta?.content;
|
|
1151
|
+
if (content) yield content;
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
/** Turn-based: send full message history, get assistant text. */
|
|
1155
|
+
async turn(messages, extra) {
|
|
1156
|
+
const completion = await this.chat({ messages, ...extra });
|
|
1157
|
+
return completion.choices[0]?.message?.content || "";
|
|
1158
|
+
}
|
|
1159
|
+
/** Clone with a new resume id — same harness, different logical conversation. */
|
|
1160
|
+
withResume(resume) {
|
|
1161
|
+
return new _BridgeSession(this.client, { ...this.cfg, resume });
|
|
1162
|
+
}
|
|
1163
|
+
/** Clone with a different model inside the same harness. */
|
|
1164
|
+
withModel(model) {
|
|
1165
|
+
return new _BridgeSession(this.client, { ...this.cfg, model });
|
|
1166
|
+
}
|
|
1167
|
+
/** The effective model id that will land on the router (`bridge/<harness>/<model>`). */
|
|
1168
|
+
get model() {
|
|
1169
|
+
return this.cfg.model ? `bridge/${this.cfg.harness}/${this.cfg.model}` : `bridge/${this.cfg.harness}`;
|
|
1170
|
+
}
|
|
1171
|
+
/** The resume id currently bound to this session, if any. */
|
|
1172
|
+
get resume() {
|
|
1173
|
+
return this.cfg.resume;
|
|
1174
|
+
}
|
|
528
1175
|
};
|
|
1176
|
+
function selectTiers(all, n) {
|
|
1177
|
+
if (n >= all.length) return [...all];
|
|
1178
|
+
if (n <= 1) return [all[0]];
|
|
1179
|
+
if (n === 2) return [all[0], all[all.length - 1]];
|
|
1180
|
+
const result = [all[0]];
|
|
1181
|
+
const step = (all.length - 1) / (n - 1);
|
|
1182
|
+
for (let i = 1; i < n - 1; i++) {
|
|
1183
|
+
result.push(all[Math.round(i * step)]);
|
|
1184
|
+
}
|
|
1185
|
+
result.push(all[all.length - 1]);
|
|
1186
|
+
return result;
|
|
1187
|
+
}
|
|
1188
|
+
function formatPrice(pricePerToken) {
|
|
1189
|
+
return `$${(pricePerToken * 1e3).toFixed(6)}/1K tokens`;
|
|
1190
|
+
}
|
|
529
1191
|
var TCloudError = class extends Error {
|
|
530
1192
|
constructor(status, message) {
|
|
531
1193
|
super(message);
|
|
@@ -831,6 +1493,7 @@ var TCloud = class _TCloud extends TCloudClient {
|
|
|
831
1493
|
var fs = __toESM(require("fs"), 1);
|
|
832
1494
|
var path = __toESM(require("path"), 1);
|
|
833
1495
|
var readline = __toESM(require("readline"), 1);
|
|
1496
|
+
var import_child_process = require("child_process");
|
|
834
1497
|
var CONFIG_DIR = path.join(process.env.HOME || "~", ".tcloud");
|
|
835
1498
|
var CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
836
1499
|
var WALLETS_FILE = path.join(CONFIG_DIR, "wallets.json");
|
|
@@ -879,23 +1542,52 @@ program.command("config").description("View or update configuration").option("--
|
|
|
879
1542
|
console.log(JSON.stringify(c, null, 2));
|
|
880
1543
|
});
|
|
881
1544
|
var auth = program.command("auth").description("Authentication");
|
|
882
|
-
|
|
1545
|
+
function openBrowser(url) {
|
|
1546
|
+
if (process.env.NO_BROWSER || process.env.CI) return false;
|
|
1547
|
+
if (!process.stdout.isTTY) return false;
|
|
1548
|
+
try {
|
|
1549
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
1550
|
+
const child = (0, import_child_process.spawn)(cmd, [url], { stdio: "ignore", detached: true });
|
|
1551
|
+
child.unref();
|
|
1552
|
+
return true;
|
|
1553
|
+
} catch {
|
|
1554
|
+
return false;
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
1558
|
+
async function runDeviceFlow(mode) {
|
|
883
1559
|
const config = loadConfig();
|
|
1560
|
+
const initRes = await fetch(`${config.apiUrl}/api/auth/device`, { method: "POST" });
|
|
1561
|
+
if (!initRes.ok) {
|
|
1562
|
+
console.error(`
|
|
1563
|
+
\u2717 Auth server returned ${initRes.status}.`);
|
|
1564
|
+
process.exit(1);
|
|
1565
|
+
}
|
|
1566
|
+
const d = await initRes.json();
|
|
1567
|
+
const urlWithCode = `${d.verification_url}?user_code=${encodeURIComponent(d.user_code)}`;
|
|
1568
|
+
const opened = openBrowser(urlWithCode);
|
|
1569
|
+
const header = mode === "signup" ? "Creating your Tangle account" : "Signing you in";
|
|
1570
|
+
console.log(`
|
|
1571
|
+
${header}
|
|
1572
|
+
`);
|
|
1573
|
+
console.log(` ${opened ? "Browser opened:" : "Open this URL:"}`);
|
|
1574
|
+
console.log(` ${urlWithCode}
|
|
1575
|
+
`);
|
|
1576
|
+
console.log(` Verification code: ${d.user_code}
|
|
1577
|
+
`);
|
|
1578
|
+
const deadline = Date.now() + (d.expires_in || 600) * 1e3;
|
|
1579
|
+
const interval = Math.max(2, d.interval || 5) * 1e3;
|
|
1580
|
+
const spinnerEnabled = process.stdout.isTTY && !process.env.CI;
|
|
1581
|
+
let frame = 0;
|
|
1582
|
+
function tickSpinner() {
|
|
1583
|
+
if (!spinnerEnabled) return;
|
|
1584
|
+
const remaining = Math.max(0, Math.round((deadline - Date.now()) / 1e3));
|
|
1585
|
+
process.stdout.write(`\r ${SPINNER_FRAMES[frame++ % SPINNER_FRAMES.length]} waiting for browser confirmation (${remaining}s remaining) `);
|
|
1586
|
+
}
|
|
1587
|
+
const spinnerTimer = spinnerEnabled ? setInterval(tickSpinner, 100) : null;
|
|
884
1588
|
try {
|
|
885
|
-
const res = await fetch(`${config.apiUrl}/api/auth/device`, { method: "POST" });
|
|
886
|
-
if (!res.ok) {
|
|
887
|
-
console.error("Auth server error");
|
|
888
|
-
process.exit(1);
|
|
889
|
-
}
|
|
890
|
-
const d = await res.json();
|
|
891
|
-
console.log(`
|
|
892
|
-
Open: ${d.verification_url}
|
|
893
|
-
Code: ${d.user_code}
|
|
894
|
-
|
|
895
|
-
Waiting...`);
|
|
896
|
-
const deadline = Date.now() + (d.expires_in || 600) * 1e3;
|
|
897
1589
|
while (Date.now() < deadline) {
|
|
898
|
-
await new Promise((r2) => setTimeout(r2,
|
|
1590
|
+
await new Promise((r2) => setTimeout(r2, interval));
|
|
899
1591
|
const r = await fetch(`${config.apiUrl}/api/auth/device/token`, {
|
|
900
1592
|
method: "POST",
|
|
901
1593
|
headers: { "Content-Type": "application/json" },
|
|
@@ -905,31 +1597,112 @@ auth.command("login").description("Log in via browser (device flow)").action(asy
|
|
|
905
1597
|
if (t.access_token) {
|
|
906
1598
|
config.apiKey = t.access_token;
|
|
907
1599
|
saveConfig(config);
|
|
908
|
-
|
|
1600
|
+
if (spinnerTimer) {
|
|
1601
|
+
clearInterval(spinnerTimer);
|
|
1602
|
+
process.stdout.write("\r" + " ".repeat(80) + "\r");
|
|
1603
|
+
}
|
|
1604
|
+
console.log(` \u2713 Authenticated`);
|
|
1605
|
+
console.log(` \u2713 API key saved to ${CONFIG_FILE}
|
|
1606
|
+
`);
|
|
1607
|
+
console.log(` Try it:`);
|
|
1608
|
+
console.log(` tcloud whoami`);
|
|
1609
|
+
console.log(` tcloud chat "hello world"
|
|
1610
|
+
`);
|
|
909
1611
|
return;
|
|
910
1612
|
}
|
|
911
1613
|
if (t.error === "expired_token") {
|
|
912
|
-
|
|
1614
|
+
if (spinnerTimer) {
|
|
1615
|
+
clearInterval(spinnerTimer);
|
|
1616
|
+
process.stdout.write("\r" + " ".repeat(80) + "\r");
|
|
1617
|
+
}
|
|
1618
|
+
console.error(` \u2717 Code expired. Run 'tcloud auth ${mode}' again.`);
|
|
913
1619
|
process.exit(1);
|
|
914
1620
|
}
|
|
915
|
-
process.stdout.write(".");
|
|
916
1621
|
}
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
1622
|
+
if (spinnerTimer) {
|
|
1623
|
+
clearInterval(spinnerTimer);
|
|
1624
|
+
process.stdout.write("\r" + " ".repeat(80) + "\r");
|
|
1625
|
+
}
|
|
1626
|
+
console.error(` \u2717 Timed out after ${Math.round((d.expires_in || 600) / 60)} minutes.`);
|
|
1627
|
+
process.exit(1);
|
|
1628
|
+
} finally {
|
|
1629
|
+
if (spinnerTimer) clearInterval(spinnerTimer);
|
|
920
1630
|
}
|
|
1631
|
+
}
|
|
1632
|
+
auth.command("signup").description("Create an account via browser (device flow)").action(() => runDeviceFlow("signup"));
|
|
1633
|
+
auth.command("login").description("Log in via browser (device flow)").action(() => runDeviceFlow("login"));
|
|
1634
|
+
auth.command("logout").description("Remove stored credentials").action(() => {
|
|
1635
|
+
const c = loadConfig();
|
|
1636
|
+
delete c.apiKey;
|
|
1637
|
+
saveConfig(c);
|
|
1638
|
+
console.log(` \u2713 Logged out. Config kept at ${CONFIG_FILE}.`);
|
|
921
1639
|
});
|
|
922
1640
|
auth.command("set-key").description("Set API key directly").argument("<key>").action((key) => {
|
|
923
1641
|
const c = loadConfig();
|
|
924
1642
|
c.apiKey = key;
|
|
925
1643
|
saveConfig(c);
|
|
926
|
-
console.log("API key saved.");
|
|
1644
|
+
console.log(" \u2713 API key saved.");
|
|
927
1645
|
});
|
|
928
1646
|
auth.command("status").description("Show auth status").action(() => {
|
|
929
1647
|
const c = loadConfig();
|
|
930
|
-
console.log(c.apiKey ? `Authenticated: ${c.apiKey.slice(0, 15)}
|
|
1648
|
+
console.log(c.apiKey ? ` \u2713 Authenticated: ${c.apiKey.slice(0, 15)}...${c.apiKey.slice(-4)}` : " \u2717 Not authenticated. Run: tcloud auth signup");
|
|
931
1649
|
const w = loadWallets();
|
|
932
|
-
if (w.length) console.log(`Shielded wallets: ${w.length}`);
|
|
1650
|
+
if (w.length) console.log(` Shielded wallets: ${w.length}`);
|
|
1651
|
+
});
|
|
1652
|
+
auth.command("whoami").description("Show logged-in account details").action(async () => {
|
|
1653
|
+
const c = loadConfig();
|
|
1654
|
+
if (!c.apiKey) {
|
|
1655
|
+
console.log(" \u2717 Not authenticated. Run: tcloud auth signup");
|
|
1656
|
+
return;
|
|
1657
|
+
}
|
|
1658
|
+
try {
|
|
1659
|
+
const res = await fetch(`${c.apiUrl}/api/auth/userinfo`, {
|
|
1660
|
+
headers: { Authorization: `Bearer ${c.apiKey}` }
|
|
1661
|
+
});
|
|
1662
|
+
if (!res.ok) {
|
|
1663
|
+
console.log(` \u2717 Auth check failed (${res.status}). Your key may be revoked \u2014 run: tcloud auth login`);
|
|
1664
|
+
return;
|
|
1665
|
+
}
|
|
1666
|
+
const me = await res.json();
|
|
1667
|
+
const user = me.user ?? {};
|
|
1668
|
+
const sub = me.subscription;
|
|
1669
|
+
console.log(` Email: ${user.email ?? "n/a"}`);
|
|
1670
|
+
console.log(` User: ${user.name ?? user.id ?? "n/a"}`);
|
|
1671
|
+
console.log(` Plan: ${sub?.plan ?? "free"}`);
|
|
1672
|
+
console.log(` Balance: $${Number(me.balance ?? 0).toFixed(4)}`);
|
|
1673
|
+
console.log(` Key: ${c.apiKey.slice(0, 15)}...${c.apiKey.slice(-4)}`);
|
|
1674
|
+
console.log(` API: ${c.apiUrl}`);
|
|
1675
|
+
} catch (e) {
|
|
1676
|
+
console.log(` \u2717 ${e.message ?? e}`);
|
|
1677
|
+
}
|
|
1678
|
+
});
|
|
1679
|
+
program.command("signup").description("Create an account via browser (alias for `auth signup`)").action(() => runDeviceFlow("signup"));
|
|
1680
|
+
program.command("login").description("Log in via browser (alias for `auth login`)").action(() => runDeviceFlow("login"));
|
|
1681
|
+
program.command("logout").description("Remove stored credentials (alias for `auth logout`)").action(() => {
|
|
1682
|
+
const c = loadConfig();
|
|
1683
|
+
delete c.apiKey;
|
|
1684
|
+
saveConfig(c);
|
|
1685
|
+
console.log(` \u2713 Logged out.`);
|
|
1686
|
+
});
|
|
1687
|
+
program.command("whoami").description("Show logged-in account (alias for `auth whoami`)").action(async () => {
|
|
1688
|
+
const c = loadConfig();
|
|
1689
|
+
if (!c.apiKey) {
|
|
1690
|
+
console.log(" \u2717 Not authenticated. Run: tcloud signup");
|
|
1691
|
+
return;
|
|
1692
|
+
}
|
|
1693
|
+
try {
|
|
1694
|
+
const res = await fetch(`${c.apiUrl}/api/auth/userinfo`, { headers: { Authorization: `Bearer ${c.apiKey}` } });
|
|
1695
|
+
if (!res.ok) {
|
|
1696
|
+
console.log(` \u2717 Auth failed (${res.status}). Run: tcloud login`);
|
|
1697
|
+
return;
|
|
1698
|
+
}
|
|
1699
|
+
const me = await res.json();
|
|
1700
|
+
const user = me.user ?? {};
|
|
1701
|
+
const sub = me.subscription;
|
|
1702
|
+
console.log(` ${user.email ?? user.name ?? user.id} \xB7 $${Number(me.balance ?? 0).toFixed(4)} \xB7 ${sub?.plan ?? "free"}`);
|
|
1703
|
+
} catch (e) {
|
|
1704
|
+
console.log(` \u2717 ${e.message ?? e}`);
|
|
1705
|
+
}
|
|
933
1706
|
});
|
|
934
1707
|
var wallet = program.command("wallet").description("Shielded wallet management");
|
|
935
1708
|
wallet.command("generate").description("Generate ephemeral wallet").option("-l, --label <name>").action((opts) => {
|
|
@@ -1050,7 +1823,10 @@ credits.command("add").description("Add credits").argument("<amount>").action(as
|
|
|
1050
1823
|
const client = getClient();
|
|
1051
1824
|
try {
|
|
1052
1825
|
const data = await client.addCredits(parseFloat(amount));
|
|
1053
|
-
|
|
1826
|
+
if (data.url) {
|
|
1827
|
+
console.log(`Checkout URL: ${data.url}`);
|
|
1828
|
+
console.log("Complete payment to add credits.");
|
|
1829
|
+
}
|
|
1054
1830
|
} catch (e) {
|
|
1055
1831
|
console.error("Error:", e.message);
|
|
1056
1832
|
}
|