@tangle-network/tcloud 0.4.6 → 0.4.8

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 CHANGED
@@ -94,7 +94,7 @@ for await (const chunk of client.askStream('write a haiku', 'kimi-code/kimi-for-
94
94
  }
95
95
  ```
96
96
 
97
- See [`examples/14-direct-cli-bridge.ts`](./examples/14-direct-cli-bridge.ts) for the full pattern. For session-resumable agentic dispatches (file edits, multi-turn coding), see [`examples/12-bridge-sessions.ts`](./examples/12-bridge-sessions.ts) which uses the router-mediated `tcloud.bridge({...})` API.
97
+ See [`examples/14-direct-cli-bridge.ts`](./examples/14-direct-cli-bridge.ts) for the full pattern. Direct cli-bridge clients also support `client.bridge({ harness, model, resume })`; in direct mode the SDK sends `<harness>/<model>` and maps `resume` to cli-bridge's `session_id`.
98
98
 
99
99
  ### Model Selection
100
100
 
@@ -379,9 +379,10 @@ tcloud config --model gpt-4o-mini
379
379
  ```
380
380
 
381
381
  Environment variables:
382
- - `TCLOUD_API_KEY` — API key (primary)
383
- - `OPENAI_API_KEY` — Fallback API key (works because the API is OpenAI-compatible)
384
- - `TCLOUD_BASE_URL` — Override API base URL
382
+ - `TANGLE_API_KEY` — API key (primary). One key for router + sandbox + all Tangle products.
383
+ - `TCLOUD_API_KEY` — Deprecated alias, still honored for backwards compatibility.
384
+ - `OPENAI_API_KEY` — Fallback API key (works because the API is OpenAI-compatible).
385
+ - `TCLOUD_BASE_URL` — Override API base URL.
385
386
 
386
387
  ## OpenAI SDK Compatibility
387
388
 
@@ -439,7 +440,7 @@ See the [`examples/`](./examples/) directory — each is a self-contained script
439
440
 
440
441
  Run any example:
441
442
  ```bash
442
- TCLOUD_API_KEY=sk-tan-... npx tsx examples/01-quick-start.ts
443
+ TANGLE_API_KEY=sk-tan-... npx tsx examples/01-quick-start.ts
443
444
  ```
444
445
 
445
446
  ## License
@@ -1,10 +1,13 @@
1
+ import {
2
+ TCloudSandbox
3
+ } from "./chunk-DRCOPW7D.js";
1
4
  import {
2
5
  createShieldedClient,
3
6
  generateWallet
4
- } from "./chunk-4ZUVGKVH.js";
7
+ } from "./chunk-YWN4JOCW.js";
5
8
  import {
6
9
  TCloudClient
7
- } from "./chunk-M5K3EFNP.js";
10
+ } from "./chunk-CVWEKCQ3.js";
8
11
 
9
12
  // src/index.ts
10
13
  import {
@@ -54,7 +57,7 @@ var TCloud = class _TCloud extends TCloudClient {
54
57
  *
55
58
  * ```ts
56
59
  * const client = TCloud.rotating({
57
- * apiKey: process.env.TCLOUD_API_KEY,
60
+ * apiKey: process.env.TANGLE_API_KEY,
58
61
  * routing: { strategy: 'min-exposure' },
59
62
  * })
60
63
  * const stats = client.getRotationStats()
@@ -72,6 +75,23 @@ var TCloud = class _TCloud extends TCloudClient {
72
75
  * ```
73
76
  */
74
77
  static generateWallet = generateWallet;
78
+ /**
79
+ * Create a Sandbox SDK client using this TCloud client's API key by default.
80
+ *
81
+ * ```ts
82
+ * const tcloud = new TCloud({ apiKey })
83
+ * const sandbox = await tcloud.sandbox().create({ name: 'runner' })
84
+ * ```
85
+ */
86
+ sandbox(config = {}) {
87
+ const apiKey = config.apiKey ?? this.apiKey;
88
+ if (!apiKey) throw new Error("TCloud.sandbox() requires an apiKey");
89
+ return new TCloudSandbox({ ...config, apiKey });
90
+ }
91
+ /** Create a standalone Sandbox SDK client. */
92
+ static sandbox(config) {
93
+ return new TCloudSandbox(config);
94
+ }
75
95
  };
76
96
 
77
97
  export {
@@ -220,6 +220,7 @@ var PrivateRouter = class {
220
220
 
221
221
  // src/client.ts
222
222
  var ROTATING_MARKER = "__tcloudRotating";
223
+ var DIRECT_CLI_BRIDGE_MARKER = "__tcloudDirectCliBridge";
223
224
  var DEFAULT_BASE_URL = "https://router.tangle.tools/v1";
224
225
  var SDK_VERSION = "0.4.0";
225
226
  async function proxiedFetch(privacy, url, init, streaming) {
@@ -269,6 +270,43 @@ var DEFAULT_RETRY = {
269
270
  };
270
271
  var DEFAULT_TIMEOUT_MS = 6e4;
271
272
  var DEFAULT_PLATFORM_URL = "https://id.tangle.tools";
273
+ var PROTECTED_PROVIDER_OPTION_KEYS = /* @__PURE__ */ new Set([
274
+ "model",
275
+ "messages",
276
+ "temperature",
277
+ "max_tokens",
278
+ "maxTokens",
279
+ "stream",
280
+ "stop",
281
+ "top_p",
282
+ "topP",
283
+ "frequency_penalty",
284
+ "frequencyPenalty",
285
+ "presence_penalty",
286
+ "presencePenalty",
287
+ "response_format",
288
+ "responseFormat",
289
+ "tools",
290
+ "tool_choice",
291
+ "toolChoice",
292
+ "gateway",
293
+ "bridge",
294
+ "agent_profile",
295
+ "agentProfile",
296
+ "session_id",
297
+ "sessionId"
298
+ ]);
299
+ function sanitizeProviderOptions(providerOptions) {
300
+ if (!providerOptions) return {};
301
+ for (const key of Object.keys(providerOptions)) {
302
+ if (PROTECTED_PROVIDER_OPTION_KEYS.has(key)) {
303
+ throw new Error(
304
+ `providerOptions cannot override protected chat field "${key}"; use the typed ChatOptions field instead`
305
+ );
306
+ }
307
+ }
308
+ return providerOptions;
309
+ }
272
310
  var TCloudClient = class _TCloudClient {
273
311
  baseURL;
274
312
  platformURL;
@@ -309,13 +347,21 @@ var TCloudClient = class _TCloudClient {
309
347
  * const reply = await client.ask('explain X', 'claude-code/sonnet')
310
348
  * ```
311
349
  *
312
- * For session-resumable agentic dispatches (file edits, multi-turn
313
- * coding), use the router-mediated `tcloud.bridge({...})` API instead
314
- * (or POST to cli-bridge directly with `session_id` in the body).
350
+ * For session-resumable agentic dispatches, use `client.bridge(...)` on
351
+ * the returned direct client. `resume` is serialized to cli-bridge's
352
+ * `session_id` body field and the model wire format stays
353
+ * `<harness>/<model>` without the router-only `bridge/` prefix.
315
354
  */
316
355
  static fromCliBridge(opts) {
317
356
  const baseURL = opts.url.replace(/\/+$/, "") + "/v1";
318
- return new _TCloudClient({ ...opts.config ?? {}, apiKey: opts.bearer, baseURL });
357
+ const client = new _TCloudClient({ ...opts.config ?? {}, apiKey: opts.bearer, baseURL });
358
+ Object.defineProperty(client, DIRECT_CLI_BRIDGE_MARKER, {
359
+ value: true,
360
+ enumerable: false,
361
+ configurable: false,
362
+ writable: false
363
+ });
364
+ return client;
319
365
  }
320
366
  /**
321
367
  * Build a client that rotates which operator serves each call. Mirrors
@@ -327,7 +373,7 @@ var TCloudClient = class _TCloudClient {
327
373
  *
328
374
  * ```ts
329
375
  * const tcloud = TCloudClient.rotating({
330
- * apiKey: process.env.TCLOUD_API_KEY,
376
+ * apiKey: process.env.TANGLE_API_KEY,
331
377
  * routing: { strategy: 'min-exposure' },
332
378
  * })
333
379
  * await tcloud.ask('hello')
@@ -368,7 +414,7 @@ var TCloudClient = class _TCloudClient {
368
414
  constructor(config = {}) {
369
415
  this.baseURL = (config.baseURL || DEFAULT_BASE_URL).replace(/\/$/, "");
370
416
  this.platformURL = (config.platformURL || DEFAULT_PLATFORM_URL).replace(/\/$/, "");
371
- this.apiKey = config.apiKey || process.env.TCLOUD_API_KEY;
417
+ this.apiKey = config.apiKey || process.env.TANGLE_API_KEY || process.env.TCLOUD_API_KEY;
372
418
  this.model = config.model || "gpt-4o-mini";
373
419
  this.privacy = config.privacy;
374
420
  this.limits = config.limits;
@@ -577,8 +623,8 @@ var TCloudClient = class _TCloudClient {
577
623
  delete headers["Authorization"];
578
624
  }
579
625
  }
580
- if (bridge) {
581
- headers["X-Bridge-Unlock"] = bridge.unlock;
626
+ if (bridge && !this._isDirectCliBridge()) {
627
+ headers["X-Bridge-Unlock"] = bridge.unlock ?? "";
582
628
  if (bridge.resume) headers["X-Resume"] = bridge.resume;
583
629
  if (bridge.bridgeUrl) headers["X-Bridge-Url"] = bridge.bridgeUrl;
584
630
  if (bridge.bridgeBearer) headers["X-Bridge-Bearer"] = bridge.bridgeBearer;
@@ -591,13 +637,24 @@ var TCloudClient = class _TCloudClient {
591
637
  */
592
638
  _effectiveModel(options) {
593
639
  if (options.bridge) {
640
+ if (this._isDirectCliBridge()) {
641
+ return options.bridge.model ? `${options.bridge.harness}/${options.bridge.model}` : options.bridge.harness;
642
+ }
594
643
  return options.bridge.model ? `bridge/${options.bridge.harness}/${options.bridge.model}` : `bridge/${options.bridge.harness}`;
595
644
  }
596
645
  return options.model || this.model;
597
646
  }
598
647
  /** Build the chat completions request body */
599
648
  _chatBody(options, stream) {
649
+ const providerOptions = sanitizeProviderOptions(options.providerOptions);
650
+ const sandboxBody = {};
651
+ if (options.sandbox?.agentProfile) sandboxBody.agent_profile = options.sandbox.agentProfile;
652
+ if (options.sandbox?.sessionId) sandboxBody.session_id = options.sandbox.sessionId;
653
+ if (this._isDirectCliBridge() && options.bridge?.resume && !sandboxBody.session_id) {
654
+ sandboxBody.session_id = options.bridge.resume;
655
+ }
600
656
  return JSON.stringify({
657
+ ...providerOptions,
601
658
  model: this._effectiveModel(options),
602
659
  messages: options.messages,
603
660
  temperature: options.temperature,
@@ -611,7 +668,7 @@ var TCloudClient = class _TCloudClient {
611
668
  tools: options.tools,
612
669
  tool_choice: options.toolChoice,
613
670
  ...options.gateway ? { gateway: options.gateway } : {},
614
- ...options.providerOptions
671
+ ...sandboxBody
615
672
  });
616
673
  }
617
674
  /** Chat completion (non-streaming) */
@@ -692,7 +749,13 @@ var TCloudClient = class _TCloudClient {
692
749
  "TCloudClient.rotating() cannot dispatch sandbox-harness sessions.\nSandbox sessions bind to a single operator; rotation is meaningful only for\nstateless calls. Use TCloudClient.shielded() + AgentProfile.confidential.tee\nfor privacy-preserving sandbox execution instead."
693
750
  );
694
751
  }
695
- return new BridgeSession(this, cfg);
752
+ if (!this._isDirectCliBridge() && cfg.unlock == null) {
753
+ throw new Error("Bridge unlock is required for router-mediated bridge sessions");
754
+ }
755
+ return new BridgeSession(this, cfg, this._isDirectCliBridge());
756
+ }
757
+ _isDirectCliBridge() {
758
+ return this[DIRECT_CLI_BRIDGE_MARKER] === true;
696
759
  }
697
760
  /**
698
761
  * Rotation stats — populated only on clients created via
@@ -858,6 +921,49 @@ var TCloudClient = class _TCloudClient {
858
921
  })
859
922
  });
860
923
  }
924
+ /**
925
+ * Edit / inpaint / variate an existing image with a text prompt.
926
+ * Sibling to `imageGenerate`; routes to `/v1/images/edits` via
927
+ * multipart/form-data per the OpenAI spec.
928
+ *
929
+ * Reference image attachments may be passed as `Blob`, `ArrayBuffer`,
930
+ * or `{data: base64, mediaType, filename?}`. Multi-image composition
931
+ * (e.g. gpt-image-2 with two reference frames + a prompt that fuses
932
+ * them) is supported by passing an array; for legacy models only the
933
+ * first image is honored upstream.
934
+ */
935
+ async imagesEdit(options) {
936
+ const formData = new FormData();
937
+ formData.append("prompt", options.prompt);
938
+ formData.append("model", options.model || "gpt-image-2");
939
+ if (options.n != null) formData.append("n", String(options.n));
940
+ if (options.size) formData.append("size", options.size);
941
+ if (options.quality) formData.append("quality", options.quality);
942
+ if (options.response_format) formData.append("response_format", options.response_format);
943
+ if (options.mask) formData.append("mask", toEditBlob(options.mask, "mask.png"), "mask.png");
944
+ const images = Array.isArray(options.image) ? options.image : [options.image];
945
+ if (images.length === 0) {
946
+ throw new TCloudError(400, "imagesEdit requires at least one image attachment");
947
+ }
948
+ images.forEach((img, idx) => {
949
+ const blob = toEditBlob(img, `image-${idx + 1}.png`);
950
+ formData.append("image[]", blob, blobFilename(img, `image-${idx + 1}.png`));
951
+ });
952
+ const headers = { ...this.headers };
953
+ delete headers["Content-Type"];
954
+ this.checkLimits();
955
+ const res = await proxiedFetch(this.privacy, `${this.baseURL}/images/edits`, {
956
+ method: "POST",
957
+ headers,
958
+ body: formData
959
+ }, false);
960
+ if (!res.ok) {
961
+ const err = await res.json().catch(() => ({ error: res.statusText }));
962
+ throw new TCloudError(res.status, err.error?.message || err.error || err.message || res.statusText);
963
+ }
964
+ this._requestCount++;
965
+ return res.json();
966
+ }
861
967
  /** Rerank documents by relevance to a query */
862
968
  async rerank(options) {
863
969
  return this._request(`${this.baseURL}/rerank`, {
@@ -949,7 +1055,7 @@ var TCloudClient = class _TCloudClient {
949
1055
  }
950
1056
  /** Get video generation status */
951
1057
  async videoStatus(id) {
952
- return this._fetch(`${this.baseURL}/video?id=${id}`);
1058
+ return this._fetch(`${this.baseURL}/video/${encodeURIComponent(id)}`);
953
1059
  }
954
1060
  /** Generate an avatar video (lip-synced talking head from audio + face image).
955
1061
  * Returns 202 with a job_id for async polling via avatarJobStatus(). */
@@ -1209,12 +1315,14 @@ var ALL_TIERS = [
1209
1315
  { name: "max-gpu-tee", cpu: 64, ramGb: 256, gpu: 4, tee: true }
1210
1316
  ];
1211
1317
  var BridgeSession = class _BridgeSession {
1212
- constructor(client, cfg) {
1318
+ constructor(client, cfg, direct = false) {
1213
1319
  this.client = client;
1214
1320
  this.cfg = cfg;
1321
+ this.direct = direct;
1215
1322
  }
1216
1323
  client;
1217
1324
  cfg;
1325
+ direct;
1218
1326
  /** Full chat completion (non-streaming). */
1219
1327
  async chat(options) {
1220
1328
  return this.client.chat({ ...options, bridge: this.cfg });
@@ -1248,15 +1356,16 @@ var BridgeSession = class _BridgeSession {
1248
1356
  }
1249
1357
  /** Clone with a new resume id — same harness, different logical conversation. */
1250
1358
  withResume(resume) {
1251
- return new _BridgeSession(this.client, { ...this.cfg, resume });
1359
+ return new _BridgeSession(this.client, { ...this.cfg, resume }, this.direct);
1252
1360
  }
1253
1361
  /** Clone with a different model inside the same harness. */
1254
1362
  withModel(model) {
1255
- return new _BridgeSession(this.client, { ...this.cfg, model });
1363
+ return new _BridgeSession(this.client, { ...this.cfg, model }, this.direct);
1256
1364
  }
1257
1365
  /** The effective model id that will land on the router (`bridge/<harness>/<model>`). */
1258
1366
  get model() {
1259
- return this.cfg.model ? `bridge/${this.cfg.harness}/${this.cfg.model}` : `bridge/${this.cfg.harness}`;
1367
+ const prefix = this.direct ? "" : "bridge/";
1368
+ return this.cfg.model ? `${prefix}${this.cfg.harness}/${this.cfg.model}` : `${prefix}${this.cfg.harness}`;
1260
1369
  }
1261
1370
  /** The resume id currently bound to this session, if any. */
1262
1371
  get resume() {
@@ -1278,6 +1387,32 @@ function selectTiers(all, n) {
1278
1387
  function formatPrice(pricePerToken) {
1279
1388
  return `$${(pricePerToken * 1e3).toFixed(6)}/1K tokens`;
1280
1389
  }
1390
+ function toEditBlob(input, defaultFilename) {
1391
+ if (input instanceof Blob) return input;
1392
+ if (input instanceof ArrayBuffer) return new Blob([input], { type: "image/png" });
1393
+ const binary = base64ToUint8Array(input.data);
1394
+ const copy = new Uint8Array(binary.byteLength);
1395
+ copy.set(binary);
1396
+ return new Blob([copy.buffer], { type: input.mediaType || "image/png" });
1397
+ void defaultFilename;
1398
+ }
1399
+ function blobFilename(input, fallback) {
1400
+ if (typeof input === "object" && !(input instanceof Blob) && !(input instanceof ArrayBuffer)) {
1401
+ if (input.filename) return input.filename;
1402
+ }
1403
+ return fallback;
1404
+ }
1405
+ function base64ToUint8Array(b64) {
1406
+ const m = /^data:[^;,]*(?:;[^,]*)?,(.+)$/.exec(b64);
1407
+ const raw = m ? m[1] : b64;
1408
+ const bin = typeof atob === "function" ? atob(raw) : (
1409
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1410
+ globalThis.Buffer.from(raw, "base64").toString("binary")
1411
+ );
1412
+ const out = new Uint8Array(bin.length);
1413
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
1414
+ return out;
1415
+ }
1281
1416
  var TCloudError = class extends Error {
1282
1417
  constructor(status, message) {
1283
1418
  super(message);
@@ -250,7 +250,14 @@ function buildSandboxCreateOptions(options) {
250
250
  memoryMB: options.memoryMb,
251
251
  diskGB: options.diskGb
252
252
  } : void 0,
253
- backend: options.backend ? { type: options.backend } : void 0,
253
+ // backend: merge `backend` (type string) with `agentProfile` (carried
254
+ // as `backend.profile` on the underlying SDK). Either may be set
255
+ // independently; setting both produces `{ type, profile }`. Setting
256
+ // neither leaves `backend` undefined so the SDK applies its default.
257
+ backend: options.backend || options.agentProfile ? {
258
+ ...options.backend ? { type: options.backend } : {},
259
+ ...options.agentProfile ? { profile: options.agentProfile } : {}
260
+ } : void 0,
254
261
  confidential: options.tee ? {
255
262
  tee: options.tee,
256
263
  sealed: options.sealed || void 0,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  TCloudClient
3
- } from "./chunk-M5K3EFNP.js";
3
+ } from "./chunk-CVWEKCQ3.js";
4
4
 
5
5
  // src/shielded.ts
6
6
  import { privateKeyToAccount } from "viem/accounts";