@tangle-network/tcloud 0.1.4 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -10
- package/dist/chunk-HL4CXKET.js +976 -0
- package/dist/{chunk-PILYAKCF.js → chunk-STNZT6YR.js} +4 -2
- package/dist/chunk-VD4RNZOC.js +263 -0
- package/dist/cli.cjs +616 -153
- package/dist/cli.js +3 -2
- package/dist/{shielded-BTi_OftW.d.cts → client-CcuHG7_w.d.cts} +275 -60
- package/dist/{shielded-BTi_OftW.d.ts → client-CcuHG7_w.d.ts} +275 -60
- package/dist/index.cjs +618 -153
- package/dist/index.d.cts +4 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +8 -4
- package/dist/instance.cjs +1235 -0
- package/dist/instance.d.cts +110 -0
- package/dist/instance.d.ts +110 -0
- package/dist/instance.js +229 -0
- package/dist/shielded.cjs +616 -153
- package/dist/shielded.d.cts +61 -2
- package/dist/shielded.d.ts +61 -2
- package/dist/shielded.js +2 -1
- package/package.json +14 -2
- package/dist/chunk-KVXWEAK3.js +0 -771
package/dist/chunk-KVXWEAK3.js
DELETED
|
@@ -1,771 +0,0 @@
|
|
|
1
|
-
// src/shielded.ts
|
|
2
|
-
import { privateKeyToAccount } from "viem/accounts";
|
|
3
|
-
import { keccak256, encodeAbiParameters, parseAbiParameters, concat, toBytes } from "viem";
|
|
4
|
-
|
|
5
|
-
// src/client.ts
|
|
6
|
-
var DEFAULT_BASE_URL = "https://router.tangle.tools/v1";
|
|
7
|
-
async function proxiedFetch(privacy, url, init, streaming) {
|
|
8
|
-
if (!privacy || privacy.mode === "direct") {
|
|
9
|
-
return fetch(url, init);
|
|
10
|
-
}
|
|
11
|
-
if (privacy.mode === "relayer") {
|
|
12
|
-
if (!privacy.relayerUrl) {
|
|
13
|
-
throw new Error('relayerUrl is required when privacy mode is "relayer"');
|
|
14
|
-
}
|
|
15
|
-
const proxyPath = streaming ? "/relay/proxy-stream" : "/relay/proxy";
|
|
16
|
-
const hdrs = {};
|
|
17
|
-
if (init.headers) {
|
|
18
|
-
const entries = init.headers instanceof Headers ? Array.from(init.headers.entries()) : Object.entries(init.headers);
|
|
19
|
-
for (const [k, v] of entries) hdrs[k] = v;
|
|
20
|
-
}
|
|
21
|
-
return fetch(`${privacy.relayerUrl}${proxyPath}`, {
|
|
22
|
-
method: "POST",
|
|
23
|
-
headers: { "Content-Type": "application/json" },
|
|
24
|
-
body: JSON.stringify({
|
|
25
|
-
target: url,
|
|
26
|
-
body: typeof init.body === "string" ? JSON.parse(init.body) : init.body,
|
|
27
|
-
headers: hdrs
|
|
28
|
-
})
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
if (privacy.mode === "socks5") {
|
|
32
|
-
if (!privacy.socksProxy) {
|
|
33
|
-
throw new Error('socksProxy is required when privacy mode is "socks5"');
|
|
34
|
-
}
|
|
35
|
-
const { SocksProxyAgent } = await import("socks-proxy-agent");
|
|
36
|
-
const agent = new SocksProxyAgent(privacy.socksProxy);
|
|
37
|
-
return fetch(url, {
|
|
38
|
-
...init,
|
|
39
|
-
// @ts-expect-error agent is supported by Node's undici but not in the standard RequestInit type
|
|
40
|
-
agent
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
return fetch(url, init);
|
|
44
|
-
}
|
|
45
|
-
var TCloudClient = class {
|
|
46
|
-
baseURL;
|
|
47
|
-
apiKey;
|
|
48
|
-
model;
|
|
49
|
-
headers;
|
|
50
|
-
spendAuthFn;
|
|
51
|
-
privacy;
|
|
52
|
-
limits;
|
|
53
|
-
_totalSpent = 0;
|
|
54
|
-
_requestCount = 0;
|
|
55
|
-
constructor(config = {}) {
|
|
56
|
-
this.baseURL = (config.baseURL || DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
57
|
-
this.apiKey = config.apiKey || process.env.TCLOUD_API_KEY || process.env.OPENAI_API_KEY;
|
|
58
|
-
this.model = config.model || "gpt-4o-mini";
|
|
59
|
-
this.privacy = config.privacy;
|
|
60
|
-
this.limits = config.limits;
|
|
61
|
-
this.headers = {
|
|
62
|
-
"Content-Type": "application/json",
|
|
63
|
-
"X-Tangle-Client": "tcloud-sdk/0.1.4"
|
|
64
|
-
};
|
|
65
|
-
if (this.apiKey) {
|
|
66
|
-
this.headers["Authorization"] = `Bearer ${this.apiKey}`;
|
|
67
|
-
}
|
|
68
|
-
if (config.routing?.mode) {
|
|
69
|
-
this.headers["X-Tangle-Routing"] = config.routing.mode;
|
|
70
|
-
}
|
|
71
|
-
if (config.routing?.prefer) {
|
|
72
|
-
this.headers["X-Tangle-Operator"] = config.routing.prefer;
|
|
73
|
-
}
|
|
74
|
-
if (config.routing?.blueprintId) {
|
|
75
|
-
this.headers["X-Tangle-Blueprint"] = config.routing.blueprintId;
|
|
76
|
-
}
|
|
77
|
-
if (config.routing?.serviceId) {
|
|
78
|
-
this.headers["X-Tangle-Service"] = config.routing.serviceId;
|
|
79
|
-
}
|
|
80
|
-
if (config.routing?.region) {
|
|
81
|
-
this.headers["X-Tangle-Region"] = config.routing.region;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
/** Set the SpendAuth signer for private mode */
|
|
85
|
-
setSpendAuthSigner(fn) {
|
|
86
|
-
this.spendAuthFn = fn;
|
|
87
|
-
}
|
|
88
|
-
/** Current metering stats */
|
|
89
|
-
get usage() {
|
|
90
|
-
return {
|
|
91
|
-
totalSpent: this._totalSpent,
|
|
92
|
-
requestCount: this._requestCount,
|
|
93
|
-
limits: this.limits ? { ...this.limits } : void 0
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
/** Check spending limits before a request. Throws TCloudError if blocked. */
|
|
97
|
-
checkLimits() {
|
|
98
|
-
if (!this.limits) return;
|
|
99
|
-
if (this.limits.maxRequests && this._requestCount >= this.limits.maxRequests) {
|
|
100
|
-
this.limits.onLimitReached?.({ type: "requests", current: this._requestCount, limit: this.limits.maxRequests });
|
|
101
|
-
throw new TCloudError(429, `Request limit reached (${this._requestCount}/${this.limits.maxRequests})`);
|
|
102
|
-
}
|
|
103
|
-
if (this.limits.maxTotalSpend && this._totalSpent >= this.limits.maxTotalSpend) {
|
|
104
|
-
this.limits.onLimitReached?.({ type: "total", current: this._totalSpent, limit: this.limits.maxTotalSpend });
|
|
105
|
-
throw new TCloudError(429, `Spending limit reached ($${this._totalSpent.toFixed(6)}/$${this.limits.maxTotalSpend})`);
|
|
106
|
-
}
|
|
107
|
-
if (this.limits.maxRequests && this.limits.onLimitWarning) {
|
|
108
|
-
const pct = this._requestCount / this.limits.maxRequests;
|
|
109
|
-
if (pct >= 0.8) this.limits.onLimitWarning({ type: "requests", current: this._requestCount, limit: this.limits.maxRequests });
|
|
110
|
-
}
|
|
111
|
-
if (this.limits.maxTotalSpend && this.limits.onLimitWarning) {
|
|
112
|
-
const pct = this._totalSpent / this.limits.maxTotalSpend;
|
|
113
|
-
if (pct >= 0.8) this.limits.onLimitWarning({ type: "total", current: this._totalSpent, limit: this.limits.maxTotalSpend });
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
/** Track cost after a response, using actual pricing from response headers when available */
|
|
117
|
-
trackCost(completion, res) {
|
|
118
|
-
this._requestCount++;
|
|
119
|
-
if (completion.usage) {
|
|
120
|
-
let estimatedCost;
|
|
121
|
-
const inputPrice = res ? parseFloat(res.headers.get("x-tangle-price-input") || "0") : 0;
|
|
122
|
-
const outputPrice = res ? parseFloat(res.headers.get("x-tangle-price-output") || "0") : 0;
|
|
123
|
-
if (inputPrice > 0 || outputPrice > 0) {
|
|
124
|
-
estimatedCost = (completion.usage.prompt_tokens || 0) * inputPrice + (completion.usage.completion_tokens || 0) * outputPrice;
|
|
125
|
-
} else {
|
|
126
|
-
const tokens = completion.usage.total_tokens || 0;
|
|
127
|
-
estimatedCost = tokens * 1e-6;
|
|
128
|
-
}
|
|
129
|
-
this._totalSpent += estimatedCost;
|
|
130
|
-
if (this.limits?.maxCostPerRequest && estimatedCost > this.limits.maxCostPerRequest) {
|
|
131
|
-
this.limits.onLimitReached?.({ type: "cost", current: estimatedCost, limit: this.limits.maxCostPerRequest });
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
/** Chat completion (non-streaming) */
|
|
136
|
-
async chat(options) {
|
|
137
|
-
this.checkLimits();
|
|
138
|
-
const headers = { ...this.headers };
|
|
139
|
-
if (this.spendAuthFn) {
|
|
140
|
-
const auth = await this.spendAuthFn();
|
|
141
|
-
headers["X-Payment-Signature"] = JSON.stringify(auth);
|
|
142
|
-
delete headers["Authorization"];
|
|
143
|
-
}
|
|
144
|
-
const res = await proxiedFetch(this.privacy, `${this.baseURL}/chat/completions`, {
|
|
145
|
-
method: "POST",
|
|
146
|
-
headers,
|
|
147
|
-
body: JSON.stringify({
|
|
148
|
-
model: options.model || this.model,
|
|
149
|
-
messages: options.messages,
|
|
150
|
-
temperature: options.temperature,
|
|
151
|
-
max_tokens: options.maxTokens,
|
|
152
|
-
stream: false,
|
|
153
|
-
stop: options.stop,
|
|
154
|
-
top_p: options.topP,
|
|
155
|
-
frequency_penalty: options.frequencyPenalty,
|
|
156
|
-
presence_penalty: options.presencePenalty,
|
|
157
|
-
response_format: options.responseFormat,
|
|
158
|
-
tools: options.tools
|
|
159
|
-
})
|
|
160
|
-
}, false);
|
|
161
|
-
if (!res.ok) {
|
|
162
|
-
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
163
|
-
throw new TCloudError(res.status, err.error?.message || err.error || err.message || res.statusText);
|
|
164
|
-
}
|
|
165
|
-
const completion = await res.json();
|
|
166
|
-
this.trackCost(completion, res);
|
|
167
|
-
return completion;
|
|
168
|
-
}
|
|
169
|
-
/** Chat completion (streaming) — returns an async iterator of chunks */
|
|
170
|
-
async *chatStream(options) {
|
|
171
|
-
this.checkLimits();
|
|
172
|
-
const headers = { ...this.headers };
|
|
173
|
-
if (this.spendAuthFn) {
|
|
174
|
-
const auth = await this.spendAuthFn();
|
|
175
|
-
headers["X-Payment-Signature"] = JSON.stringify(auth);
|
|
176
|
-
delete headers["Authorization"];
|
|
177
|
-
}
|
|
178
|
-
const res = await proxiedFetch(this.privacy, `${this.baseURL}/chat/completions`, {
|
|
179
|
-
method: "POST",
|
|
180
|
-
headers,
|
|
181
|
-
body: JSON.stringify({
|
|
182
|
-
model: options.model || this.model,
|
|
183
|
-
messages: options.messages,
|
|
184
|
-
temperature: options.temperature,
|
|
185
|
-
max_tokens: options.maxTokens,
|
|
186
|
-
stream: true,
|
|
187
|
-
stop: options.stop,
|
|
188
|
-
top_p: options.topP
|
|
189
|
-
})
|
|
190
|
-
}, true);
|
|
191
|
-
if (!res.ok) {
|
|
192
|
-
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
193
|
-
throw new TCloudError(res.status, err.error || err.message || res.statusText);
|
|
194
|
-
}
|
|
195
|
-
const reader = res.body.getReader();
|
|
196
|
-
const decoder = new TextDecoder();
|
|
197
|
-
let buf = "";
|
|
198
|
-
while (true) {
|
|
199
|
-
const { done, value } = await reader.read();
|
|
200
|
-
if (done) break;
|
|
201
|
-
buf += decoder.decode(value, { stream: true });
|
|
202
|
-
const lines = buf.split("\n");
|
|
203
|
-
buf = lines.pop() || "";
|
|
204
|
-
for (const line of lines) {
|
|
205
|
-
if (!line.startsWith("data: ")) continue;
|
|
206
|
-
const data = line.slice(6).trim();
|
|
207
|
-
if (data === "[DONE]") {
|
|
208
|
-
this._requestCount++;
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
try {
|
|
212
|
-
yield JSON.parse(data);
|
|
213
|
-
} catch {
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
/** Convenience: send a single message and get the text response */
|
|
219
|
-
async ask(message, modelOrOptions) {
|
|
220
|
-
const options = typeof modelOrOptions === "string" ? { model: modelOrOptions } : modelOrOptions;
|
|
221
|
-
const completion = await this.chat({
|
|
222
|
-
messages: [{ role: "user", content: message }],
|
|
223
|
-
...options
|
|
224
|
-
});
|
|
225
|
-
return completion.choices[0]?.message?.content || "";
|
|
226
|
-
}
|
|
227
|
-
/** Convenience: send a single message and get the full completion (with usage) */
|
|
228
|
-
async askFull(message, modelOrOptions) {
|
|
229
|
-
const options = typeof modelOrOptions === "string" ? { model: modelOrOptions } : modelOrOptions;
|
|
230
|
-
return this.chat({
|
|
231
|
-
messages: [{ role: "user", content: message }],
|
|
232
|
-
...options
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
/** Convenience: stream a single message and yield text chunks */
|
|
236
|
-
async *askStream(message, modelOrOptions) {
|
|
237
|
-
const options = typeof modelOrOptions === "string" ? { model: modelOrOptions } : modelOrOptions;
|
|
238
|
-
for await (const chunk of this.chatStream({
|
|
239
|
-
messages: [{ role: "user", content: message }],
|
|
240
|
-
...options
|
|
241
|
-
})) {
|
|
242
|
-
const content = chunk.choices[0]?.delta?.content;
|
|
243
|
-
if (content) yield content;
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
/** List available models */
|
|
247
|
-
async models() {
|
|
248
|
-
const res = await proxiedFetch(this.privacy, `${this.baseURL}/models`, { headers: this.headers }, false);
|
|
249
|
-
if (!res.ok) throw new TCloudError(res.status, "Failed to fetch models");
|
|
250
|
-
const data = await res.json();
|
|
251
|
-
return data.data || [];
|
|
252
|
-
}
|
|
253
|
-
/** List active operators */
|
|
254
|
-
async operators() {
|
|
255
|
-
const apiRoot = this.baseURL.replace(/\/v1$/, "");
|
|
256
|
-
const res = await proxiedFetch(this.privacy, `${apiRoot}/api/operators`, { headers: this.headers }, false);
|
|
257
|
-
if (!res.ok) throw new TCloudError(res.status, "Failed to fetch operators");
|
|
258
|
-
return res.json();
|
|
259
|
-
}
|
|
260
|
-
/** Get credit balance */
|
|
261
|
-
async credits() {
|
|
262
|
-
const apiRoot = this.baseURL.replace(/\/v1$/, "");
|
|
263
|
-
const res = await proxiedFetch(this.privacy, `${apiRoot}/api/billing`, { headers: this.headers }, false);
|
|
264
|
-
if (!res.ok) throw new TCloudError(res.status, "Failed to fetch credits");
|
|
265
|
-
return res.json();
|
|
266
|
-
}
|
|
267
|
-
/** Add credits */
|
|
268
|
-
async addCredits(amount) {
|
|
269
|
-
const apiRoot = this.baseURL.replace(/\/v1$/, "");
|
|
270
|
-
const res = await proxiedFetch(this.privacy, `${apiRoot}/api/billing`, {
|
|
271
|
-
method: "POST",
|
|
272
|
-
headers: this.headers,
|
|
273
|
-
body: JSON.stringify({ amount })
|
|
274
|
-
}, false);
|
|
275
|
-
if (!res.ok) throw new TCloudError(res.status, "Failed to add credits");
|
|
276
|
-
return res.json();
|
|
277
|
-
}
|
|
278
|
-
/** Create a new API key */
|
|
279
|
-
async createKey(name) {
|
|
280
|
-
const apiRoot = this.baseURL.replace(/\/v1$/, "");
|
|
281
|
-
const res = await proxiedFetch(this.privacy, `${apiRoot}/api/keys`, {
|
|
282
|
-
method: "POST",
|
|
283
|
-
headers: this.headers,
|
|
284
|
-
body: JSON.stringify({ name })
|
|
285
|
-
}, false);
|
|
286
|
-
if (!res.ok) throw new TCloudError(res.status, "Failed to create API key");
|
|
287
|
-
return res.json();
|
|
288
|
-
}
|
|
289
|
-
/** List API keys */
|
|
290
|
-
async keys() {
|
|
291
|
-
const apiRoot = this.baseURL.replace(/\/v1$/, "");
|
|
292
|
-
const res = await proxiedFetch(this.privacy, `${apiRoot}/api/keys`, { headers: this.headers }, false);
|
|
293
|
-
if (!res.ok) throw new TCloudError(res.status, "Failed to fetch keys");
|
|
294
|
-
return res.json();
|
|
295
|
-
}
|
|
296
|
-
/** Revoke an API key */
|
|
297
|
-
async revokeKey(id) {
|
|
298
|
-
const apiRoot = this.baseURL.replace(/\/v1$/, "");
|
|
299
|
-
const res = await proxiedFetch(this.privacy, `${apiRoot}/api/keys/${id}`, {
|
|
300
|
-
method: "DELETE",
|
|
301
|
-
headers: this.headers
|
|
302
|
-
}, false);
|
|
303
|
-
if (!res.ok) throw new TCloudError(res.status, "Failed to revoke key");
|
|
304
|
-
}
|
|
305
|
-
/** Generate embeddings */
|
|
306
|
-
async embeddings(options) {
|
|
307
|
-
const res = await proxiedFetch(this.privacy, `${this.baseURL}/embeddings`, {
|
|
308
|
-
method: "POST",
|
|
309
|
-
headers: this.headers,
|
|
310
|
-
body: JSON.stringify({
|
|
311
|
-
model: options.model || "text-embedding-3-small",
|
|
312
|
-
input: options.input
|
|
313
|
-
})
|
|
314
|
-
}, false);
|
|
315
|
-
if (!res.ok) {
|
|
316
|
-
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
317
|
-
throw new TCloudError(res.status, err.error?.message || err.error || res.statusText);
|
|
318
|
-
}
|
|
319
|
-
this._requestCount++;
|
|
320
|
-
return res.json();
|
|
321
|
-
}
|
|
322
|
-
/** Generate images */
|
|
323
|
-
async imageGenerate(options) {
|
|
324
|
-
const res = await proxiedFetch(this.privacy, `${this.baseURL}/images/generations`, {
|
|
325
|
-
method: "POST",
|
|
326
|
-
headers: this.headers,
|
|
327
|
-
body: JSON.stringify({
|
|
328
|
-
model: options.model || "dall-e-3",
|
|
329
|
-
prompt: options.prompt,
|
|
330
|
-
n: options.n,
|
|
331
|
-
size: options.size,
|
|
332
|
-
quality: options.quality,
|
|
333
|
-
response_format: options.response_format
|
|
334
|
-
})
|
|
335
|
-
}, false);
|
|
336
|
-
if (!res.ok) {
|
|
337
|
-
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
338
|
-
throw new TCloudError(res.status, err.error?.message || err.error || res.statusText);
|
|
339
|
-
}
|
|
340
|
-
this._requestCount++;
|
|
341
|
-
return res.json();
|
|
342
|
-
}
|
|
343
|
-
/** Rerank documents by relevance to a query */
|
|
344
|
-
async rerank(options) {
|
|
345
|
-
const res = await proxiedFetch(this.privacy, `${this.baseURL}/rerank`, {
|
|
346
|
-
method: "POST",
|
|
347
|
-
headers: this.headers,
|
|
348
|
-
body: JSON.stringify({
|
|
349
|
-
model: options.model || "rerank-english-v3.0",
|
|
350
|
-
query: options.query,
|
|
351
|
-
documents: options.documents,
|
|
352
|
-
top_n: options.top_n
|
|
353
|
-
})
|
|
354
|
-
}, false);
|
|
355
|
-
if (!res.ok) {
|
|
356
|
-
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
357
|
-
throw new TCloudError(res.status, err.error?.message || err.error || res.statusText);
|
|
358
|
-
}
|
|
359
|
-
this._requestCount++;
|
|
360
|
-
return res.json();
|
|
361
|
-
}
|
|
362
|
-
/** Text-to-speech */
|
|
363
|
-
async speech(options) {
|
|
364
|
-
const res = await proxiedFetch(this.privacy, `${this.baseURL}/audio/speech`, {
|
|
365
|
-
method: "POST",
|
|
366
|
-
headers: this.headers,
|
|
367
|
-
body: JSON.stringify({
|
|
368
|
-
model: options.model || "tts-1",
|
|
369
|
-
input: options.input,
|
|
370
|
-
voice: options.voice || "alloy"
|
|
371
|
-
})
|
|
372
|
-
}, false);
|
|
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.arrayBuffer();
|
|
379
|
-
}
|
|
380
|
-
/** Legacy completions endpoint */
|
|
381
|
-
async completions(options) {
|
|
382
|
-
const res = await proxiedFetch(this.privacy, `${this.baseURL}/completions`, {
|
|
383
|
-
method: "POST",
|
|
384
|
-
headers: this.headers,
|
|
385
|
-
body: JSON.stringify({
|
|
386
|
-
model: options.model || this.model,
|
|
387
|
-
prompt: options.prompt,
|
|
388
|
-
temperature: options.temperature,
|
|
389
|
-
max_tokens: options.maxTokens,
|
|
390
|
-
stop: options.stop,
|
|
391
|
-
top_p: options.topP
|
|
392
|
-
})
|
|
393
|
-
}, false);
|
|
394
|
-
if (!res.ok) {
|
|
395
|
-
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
396
|
-
throw new TCloudError(res.status, err.error?.message || err.error || res.statusText);
|
|
397
|
-
}
|
|
398
|
-
this._requestCount++;
|
|
399
|
-
return res.json();
|
|
400
|
-
}
|
|
401
|
-
/** Audio transcription (speech-to-text) */
|
|
402
|
-
async transcribe(file, options) {
|
|
403
|
-
const formData = new FormData();
|
|
404
|
-
formData.append("file", file, "audio.webm");
|
|
405
|
-
formData.append("model", options?.model || "whisper-1");
|
|
406
|
-
if (options?.language) formData.append("language", options.language);
|
|
407
|
-
if (options?.prompt) formData.append("prompt", options.prompt);
|
|
408
|
-
const headers = { ...this.headers };
|
|
409
|
-
delete headers["Content-Type"];
|
|
410
|
-
const res = await proxiedFetch(this.privacy, `${this.baseURL}/audio/transcriptions`, {
|
|
411
|
-
method: "POST",
|
|
412
|
-
headers,
|
|
413
|
-
body: formData
|
|
414
|
-
}, false);
|
|
415
|
-
if (!res.ok) {
|
|
416
|
-
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
417
|
-
throw new TCloudError(res.status, err.error?.message || err.error || res.statusText);
|
|
418
|
-
}
|
|
419
|
-
this._requestCount++;
|
|
420
|
-
return res.json();
|
|
421
|
-
}
|
|
422
|
-
/** Create a fine-tuning job */
|
|
423
|
-
async fineTuneCreate(options) {
|
|
424
|
-
const res = await proxiedFetch(this.privacy, `${this.baseURL}/fine_tuning/jobs`, {
|
|
425
|
-
method: "POST",
|
|
426
|
-
headers: this.headers,
|
|
427
|
-
body: JSON.stringify(options)
|
|
428
|
-
}, false);
|
|
429
|
-
if (!res.ok) {
|
|
430
|
-
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
431
|
-
throw new TCloudError(res.status, err.error?.message || err.error || res.statusText);
|
|
432
|
-
}
|
|
433
|
-
this._requestCount++;
|
|
434
|
-
return res.json();
|
|
435
|
-
}
|
|
436
|
-
/** List fine-tuning jobs */
|
|
437
|
-
async fineTuneList() {
|
|
438
|
-
const res = await proxiedFetch(this.privacy, `${this.baseURL}/fine_tuning/jobs`, {
|
|
439
|
-
headers: this.headers
|
|
440
|
-
}, false);
|
|
441
|
-
if (!res.ok) throw new TCloudError(res.status, "Failed to fetch fine-tuning jobs");
|
|
442
|
-
return res.json();
|
|
443
|
-
}
|
|
444
|
-
/** Submit a batch of chat requests */
|
|
445
|
-
async batch(requests) {
|
|
446
|
-
const res = await proxiedFetch(this.privacy, `${this.baseURL}/batch`, {
|
|
447
|
-
method: "POST",
|
|
448
|
-
headers: this.headers,
|
|
449
|
-
body: JSON.stringify({ requests })
|
|
450
|
-
}, false);
|
|
451
|
-
if (!res.ok) {
|
|
452
|
-
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
453
|
-
throw new TCloudError(res.status, err.error?.message || err.error || res.statusText);
|
|
454
|
-
}
|
|
455
|
-
return res.json();
|
|
456
|
-
}
|
|
457
|
-
/** Get batch job status */
|
|
458
|
-
async batchStatus(jobId) {
|
|
459
|
-
const res = await proxiedFetch(this.privacy, `${this.baseURL}/batch?id=${jobId}`, {
|
|
460
|
-
headers: this.headers
|
|
461
|
-
}, false);
|
|
462
|
-
if (!res.ok) throw new TCloudError(res.status, "Failed to fetch batch status");
|
|
463
|
-
return res.json();
|
|
464
|
-
}
|
|
465
|
-
/** Generate video */
|
|
466
|
-
async videoGenerate(options) {
|
|
467
|
-
const res = await proxiedFetch(this.privacy, `${this.baseURL}/video/generate`, {
|
|
468
|
-
method: "POST",
|
|
469
|
-
headers: this.headers,
|
|
470
|
-
body: JSON.stringify(options)
|
|
471
|
-
}, false);
|
|
472
|
-
if (!res.ok) {
|
|
473
|
-
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
474
|
-
throw new TCloudError(res.status, err.error?.message || err.error || res.statusText);
|
|
475
|
-
}
|
|
476
|
-
this._requestCount++;
|
|
477
|
-
return res.json();
|
|
478
|
-
}
|
|
479
|
-
/** Get video generation status */
|
|
480
|
-
async videoStatus(id) {
|
|
481
|
-
const res = await proxiedFetch(this.privacy, `${this.baseURL}/video?id=${id}`, {
|
|
482
|
-
headers: this.headers
|
|
483
|
-
}, false);
|
|
484
|
-
if (!res.ok) throw new TCloudError(res.status, "Failed to fetch video status");
|
|
485
|
-
return res.json();
|
|
486
|
-
}
|
|
487
|
-
/** Search models by name, provider, or capability */
|
|
488
|
-
async searchModels(query) {
|
|
489
|
-
const all = await this.models();
|
|
490
|
-
const q = query.toLowerCase();
|
|
491
|
-
return all.filter(
|
|
492
|
-
(m) => m.id.toLowerCase().includes(q) || m.name.toLowerCase().includes(q) || m._provider && m._provider.toLowerCase().includes(q)
|
|
493
|
-
);
|
|
494
|
-
}
|
|
495
|
-
/** Estimate cost for a request (without sending it) */
|
|
496
|
-
async estimateCost(options) {
|
|
497
|
-
const models = await this.models();
|
|
498
|
-
const model = models.find((m) => m.id === (options.model || this.model));
|
|
499
|
-
if (!model) return { inputCost: 0, outputCost: 0, total: 0 };
|
|
500
|
-
const inputCost = options.inputTokens * parseFloat(model.pricing.prompt);
|
|
501
|
-
const outputCost = options.outputTokens * parseFloat(model.pricing.completion);
|
|
502
|
-
return { inputCost, outputCost, total: inputCost + outputCost };
|
|
503
|
-
}
|
|
504
|
-
};
|
|
505
|
-
var TCloudError = class extends Error {
|
|
506
|
-
constructor(status, message) {
|
|
507
|
-
super(message);
|
|
508
|
-
this.status = status;
|
|
509
|
-
this.name = "TCloudError";
|
|
510
|
-
}
|
|
511
|
-
};
|
|
512
|
-
|
|
513
|
-
// src/shielded.ts
|
|
514
|
-
var SPEND_TYPEHASH = keccak256(
|
|
515
|
-
toBytes(
|
|
516
|
-
"SpendAuthorization(bytes32 commitment,uint64 serviceId,uint8 jobIndex,uint256 amount,address operator,uint256 nonce,uint64 expiry)"
|
|
517
|
-
)
|
|
518
|
-
);
|
|
519
|
-
var DEFAULT_DOMAIN = {
|
|
520
|
-
name: "ShieldedCredits",
|
|
521
|
-
version: "1"
|
|
522
|
-
};
|
|
523
|
-
function generateWallet() {
|
|
524
|
-
const privateKeyBytes = crypto.getRandomValues(new Uint8Array(32));
|
|
525
|
-
const privateKey = "0x" + Array.from(privateKeyBytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
526
|
-
const saltBytes = crypto.getRandomValues(new Uint8Array(32));
|
|
527
|
-
const salt = "0x" + Array.from(saltBytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
528
|
-
const account = privateKeyToAccount(privateKey);
|
|
529
|
-
const commitment = keccak256(
|
|
530
|
-
encodeAbiParameters(
|
|
531
|
-
parseAbiParameters("address, bytes32"),
|
|
532
|
-
[account.address, salt]
|
|
533
|
-
)
|
|
534
|
-
);
|
|
535
|
-
return { privateKey, address: account.address, commitment, salt };
|
|
536
|
-
}
|
|
537
|
-
async function signSpendAuth(wallet, params) {
|
|
538
|
-
const account = privateKeyToAccount(wallet.privateKey);
|
|
539
|
-
const domainSeparator = keccak256(
|
|
540
|
-
encodeAbiParameters(
|
|
541
|
-
parseAbiParameters("bytes32, bytes32, bytes32, uint256, address"),
|
|
542
|
-
[
|
|
543
|
-
keccak256(toBytes("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")),
|
|
544
|
-
keccak256(toBytes(DEFAULT_DOMAIN.name)),
|
|
545
|
-
keccak256(toBytes(DEFAULT_DOMAIN.version)),
|
|
546
|
-
BigInt(params.chainId),
|
|
547
|
-
params.creditsAddress
|
|
548
|
-
]
|
|
549
|
-
)
|
|
550
|
-
);
|
|
551
|
-
const structHash = keccak256(
|
|
552
|
-
encodeAbiParameters(
|
|
553
|
-
parseAbiParameters("bytes32, bytes32, uint64, uint8, uint256, address, uint256, uint64"),
|
|
554
|
-
[
|
|
555
|
-
SPEND_TYPEHASH,
|
|
556
|
-
wallet.commitment,
|
|
557
|
-
params.serviceId,
|
|
558
|
-
params.jobIndex,
|
|
559
|
-
params.amount,
|
|
560
|
-
params.operator,
|
|
561
|
-
params.nonce,
|
|
562
|
-
params.expiry
|
|
563
|
-
]
|
|
564
|
-
)
|
|
565
|
-
);
|
|
566
|
-
const digest = keccak256(
|
|
567
|
-
concat([toBytes("0x1901"), toBytes(domainSeparator), toBytes(structHash)])
|
|
568
|
-
);
|
|
569
|
-
const signature = await account.sign({ hash: digest });
|
|
570
|
-
return {
|
|
571
|
-
commitment: wallet.commitment,
|
|
572
|
-
serviceId: params.serviceId.toString(),
|
|
573
|
-
jobIndex: params.jobIndex,
|
|
574
|
-
amount: params.amount.toString(),
|
|
575
|
-
operator: params.operator,
|
|
576
|
-
nonce: params.nonce.toString(),
|
|
577
|
-
expiry: params.expiry.toString(),
|
|
578
|
-
signature
|
|
579
|
-
};
|
|
580
|
-
}
|
|
581
|
-
function estimateCost(inputTokens, maxOutputTokens, inputPricePerM = 0.15, outputPricePerM = 0.6) {
|
|
582
|
-
const cost = inputTokens / 1e6 * inputPricePerM + maxOutputTokens / 1e6 * outputPricePerM;
|
|
583
|
-
return BigInt(Math.ceil(cost * 1e6));
|
|
584
|
-
}
|
|
585
|
-
function createShieldedClient(config = {}) {
|
|
586
|
-
const wallet = config.wallet || generateWallet();
|
|
587
|
-
const chainId = config.chainId || 3799;
|
|
588
|
-
const creditsAddress = config.creditsAddress || "0x0000000000000000000000000000000000000000";
|
|
589
|
-
const operatorAddress = config.operatorAddress || "0x0000000000000000000000000000000000000000";
|
|
590
|
-
const serviceId = config.serviceId || 1n;
|
|
591
|
-
let nonce = 0n;
|
|
592
|
-
const client = new TCloudClient({
|
|
593
|
-
...config,
|
|
594
|
-
apiKey: void 0
|
|
595
|
-
// no API key in private mode
|
|
596
|
-
});
|
|
597
|
-
client.setSpendAuthSigner(async () => {
|
|
598
|
-
const currentNonce = nonce++;
|
|
599
|
-
const amount = estimateCost(500, 4096);
|
|
600
|
-
const buffered = amount + amount / 5n;
|
|
601
|
-
return signSpendAuth(wallet, {
|
|
602
|
-
serviceId,
|
|
603
|
-
jobIndex: 0,
|
|
604
|
-
amount: buffered,
|
|
605
|
-
operator: operatorAddress,
|
|
606
|
-
nonce: currentNonce,
|
|
607
|
-
expiry: BigInt(Math.floor(Date.now() / 1e3) + 300),
|
|
608
|
-
chainId,
|
|
609
|
-
creditsAddress
|
|
610
|
-
});
|
|
611
|
-
});
|
|
612
|
-
const monitor = { lastBalance: 0n, timer: null, replenishing: false };
|
|
613
|
-
if (config.autoReplenish) {
|
|
614
|
-
const ar = config.autoReplenish;
|
|
615
|
-
const intervalMs = ar.checkIntervalMs ?? 3e4;
|
|
616
|
-
const check = async () => {
|
|
617
|
-
try {
|
|
618
|
-
const balance = await fetchBalance(wallet.commitment, creditsAddress, chainId);
|
|
619
|
-
monitor.lastBalance = balance;
|
|
620
|
-
if (balance < ar.minBalance && !monitor.replenishing) {
|
|
621
|
-
monitor.replenishing = true;
|
|
622
|
-
try {
|
|
623
|
-
if (ar.fundingSource === "relayer") {
|
|
624
|
-
await replenishViaRelayer(ar.relayerUrl, wallet.commitment, wallet.privateKey);
|
|
625
|
-
} else {
|
|
626
|
-
await replenishDirect(
|
|
627
|
-
ar.fundingWalletKey,
|
|
628
|
-
ar.tokenAddress,
|
|
629
|
-
ar.replenishAmount,
|
|
630
|
-
wallet.commitment,
|
|
631
|
-
wallet.address,
|
|
632
|
-
creditsAddress,
|
|
633
|
-
chainId
|
|
634
|
-
);
|
|
635
|
-
}
|
|
636
|
-
monitor.lastBalance = await fetchBalance(wallet.commitment, creditsAddress, chainId);
|
|
637
|
-
console.log(`[tcloud/shielded] replenished. balance=${monitor.lastBalance}`);
|
|
638
|
-
} finally {
|
|
639
|
-
monitor.replenishing = false;
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
} catch (err) {
|
|
643
|
-
console.error("[tcloud/shielded] auto-replenish error:", err instanceof Error ? err.message : String(err));
|
|
644
|
-
}
|
|
645
|
-
};
|
|
646
|
-
void check();
|
|
647
|
-
monitor.timer = setInterval(() => void check(), intervalMs);
|
|
648
|
-
}
|
|
649
|
-
function stopAutoReplenish() {
|
|
650
|
-
if (monitor.timer) {
|
|
651
|
-
clearInterval(monitor.timer);
|
|
652
|
-
monitor.timer = null;
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
return Object.assign(client, { wallet, stopAutoReplenish });
|
|
656
|
-
}
|
|
657
|
-
var GET_ACCOUNT_ABI = [{
|
|
658
|
-
type: "function",
|
|
659
|
-
name: "getAccount",
|
|
660
|
-
inputs: [{ name: "commitment", type: "bytes32" }],
|
|
661
|
-
outputs: [{
|
|
662
|
-
name: "",
|
|
663
|
-
type: "tuple",
|
|
664
|
-
components: [
|
|
665
|
-
{ name: "spendingKey", type: "address" },
|
|
666
|
-
{ name: "token", type: "address" },
|
|
667
|
-
{ name: "balance", type: "uint256" },
|
|
668
|
-
{ name: "totalFunded", type: "uint256" },
|
|
669
|
-
{ name: "totalSpent", type: "uint256" },
|
|
670
|
-
{ name: "nonce", type: "uint256" }
|
|
671
|
-
]
|
|
672
|
-
}],
|
|
673
|
-
stateMutability: "view"
|
|
674
|
-
}];
|
|
675
|
-
var FUND_CREDITS_ABI = [{
|
|
676
|
-
type: "function",
|
|
677
|
-
name: "fundCredits",
|
|
678
|
-
inputs: [
|
|
679
|
-
{ name: "token", type: "address" },
|
|
680
|
-
{ name: "amount", type: "uint256" },
|
|
681
|
-
{ name: "commitment", type: "bytes32" },
|
|
682
|
-
{ name: "spendingKey", type: "address" }
|
|
683
|
-
],
|
|
684
|
-
outputs: [],
|
|
685
|
-
stateMutability: "nonpayable"
|
|
686
|
-
}];
|
|
687
|
-
var ERC20_APPROVE_ABI = [{
|
|
688
|
-
type: "function",
|
|
689
|
-
name: "approve",
|
|
690
|
-
inputs: [
|
|
691
|
-
{ name: "spender", type: "address" },
|
|
692
|
-
{ name: "amount", type: "uint256" }
|
|
693
|
-
],
|
|
694
|
-
outputs: [{ name: "", type: "bool" }],
|
|
695
|
-
stateMutability: "nonpayable"
|
|
696
|
-
}];
|
|
697
|
-
function makeChain(chainId, rpcUrl) {
|
|
698
|
-
return {
|
|
699
|
-
id: chainId,
|
|
700
|
-
name: `chain-${chainId}`,
|
|
701
|
-
nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
|
|
702
|
-
rpcUrls: { default: { http: [rpcUrl] } }
|
|
703
|
-
};
|
|
704
|
-
}
|
|
705
|
-
function getRpcUrl(chainId) {
|
|
706
|
-
if (chainId === 3799) return "https://testnet-rpc.tangle.tools";
|
|
707
|
-
if (chainId === 5845) return "https://rpc.tangle.tools";
|
|
708
|
-
return "http://localhost:8545";
|
|
709
|
-
}
|
|
710
|
-
async function fetchBalance(commitment, creditsAddress, chainId) {
|
|
711
|
-
const { createPublicClient, http } = await import("viem");
|
|
712
|
-
const rpcUrl = getRpcUrl(chainId);
|
|
713
|
-
const client = createPublicClient({ chain: makeChain(chainId, rpcUrl), transport: http(rpcUrl) });
|
|
714
|
-
const result = await client.readContract({
|
|
715
|
-
address: creditsAddress,
|
|
716
|
-
abi: GET_ACCOUNT_ABI,
|
|
717
|
-
functionName: "getAccount",
|
|
718
|
-
args: [commitment]
|
|
719
|
-
});
|
|
720
|
-
return result.balance;
|
|
721
|
-
}
|
|
722
|
-
async function replenishViaRelayer(relayerUrl, commitment, spendingKey) {
|
|
723
|
-
const res = await fetch(`${relayerUrl.replace(/\/$/, "")}/relay/fund-credits`, {
|
|
724
|
-
method: "POST",
|
|
725
|
-
headers: { "Content-Type": "application/json" },
|
|
726
|
-
body: JSON.stringify({
|
|
727
|
-
anchorProof: { proof: "0x", auxPublicInputs: "0x", externalData: "0x", publicInputs: "0x", encryptions: "0x" },
|
|
728
|
-
commitment,
|
|
729
|
-
spendingKey
|
|
730
|
-
})
|
|
731
|
-
});
|
|
732
|
-
if (!res.ok) {
|
|
733
|
-
const body = await res.text();
|
|
734
|
-
throw new Error(`relayer fund-credits failed (${res.status}): ${body}`);
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
async function replenishDirect(fundingKey, tokenAddress, amount, commitment, spendingKeyAddress, creditsAddress, chainId) {
|
|
738
|
-
const { createPublicClient, createWalletClient, http } = await import("viem");
|
|
739
|
-
const { privateKeyToAccount: toAccount } = await import("viem/accounts");
|
|
740
|
-
const rpcUrl = getRpcUrl(chainId);
|
|
741
|
-
const chain = makeChain(chainId, rpcUrl);
|
|
742
|
-
const account = toAccount(fundingKey);
|
|
743
|
-
const pub = createPublicClient({ chain, transport: http(rpcUrl) });
|
|
744
|
-
const wal = createWalletClient({ account, chain, transport: http(rpcUrl) });
|
|
745
|
-
const approveHash = await wal.writeContract({
|
|
746
|
-
address: tokenAddress,
|
|
747
|
-
abi: ERC20_APPROVE_ABI,
|
|
748
|
-
functionName: "approve",
|
|
749
|
-
args: [creditsAddress, amount]
|
|
750
|
-
});
|
|
751
|
-
await pub.waitForTransactionReceipt({ hash: approveHash });
|
|
752
|
-
const fundHash = await wal.writeContract({
|
|
753
|
-
address: creditsAddress,
|
|
754
|
-
abi: FUND_CREDITS_ABI,
|
|
755
|
-
functionName: "fundCredits",
|
|
756
|
-
args: [tokenAddress, amount, commitment, spendingKeyAddress]
|
|
757
|
-
});
|
|
758
|
-
const receipt = await pub.waitForTransactionReceipt({ hash: fundHash });
|
|
759
|
-
if (receipt.status !== "success") {
|
|
760
|
-
throw new Error(`fundCredits reverted (tx: ${fundHash})`);
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
export {
|
|
765
|
-
TCloudClient,
|
|
766
|
-
TCloudError,
|
|
767
|
-
generateWallet,
|
|
768
|
-
signSpendAuth,
|
|
769
|
-
estimateCost,
|
|
770
|
-
createShieldedClient
|
|
771
|
-
};
|