@tangle-network/tcloud 0.4.5 → 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/dist/instance.cjs CHANGED
@@ -263,6 +263,7 @@ var PrivateRouter = class {
263
263
 
264
264
  // src/client.ts
265
265
  var ROTATING_MARKER = "__tcloudRotating";
266
+ var DIRECT_CLI_BRIDGE_MARKER = "__tcloudDirectCliBridge";
266
267
  var DEFAULT_BASE_URL = "https://router.tangle.tools/v1";
267
268
  var SDK_VERSION = "0.4.0";
268
269
  async function proxiedFetch(privacy, url, init, streaming) {
@@ -312,6 +313,43 @@ var DEFAULT_RETRY = {
312
313
  };
313
314
  var DEFAULT_TIMEOUT_MS = 6e4;
314
315
  var DEFAULT_PLATFORM_URL = "https://id.tangle.tools";
316
+ var PROTECTED_PROVIDER_OPTION_KEYS = /* @__PURE__ */ new Set([
317
+ "model",
318
+ "messages",
319
+ "temperature",
320
+ "max_tokens",
321
+ "maxTokens",
322
+ "stream",
323
+ "stop",
324
+ "top_p",
325
+ "topP",
326
+ "frequency_penalty",
327
+ "frequencyPenalty",
328
+ "presence_penalty",
329
+ "presencePenalty",
330
+ "response_format",
331
+ "responseFormat",
332
+ "tools",
333
+ "tool_choice",
334
+ "toolChoice",
335
+ "gateway",
336
+ "bridge",
337
+ "agent_profile",
338
+ "agentProfile",
339
+ "session_id",
340
+ "sessionId"
341
+ ]);
342
+ function sanitizeProviderOptions(providerOptions) {
343
+ if (!providerOptions) return {};
344
+ for (const key of Object.keys(providerOptions)) {
345
+ if (PROTECTED_PROVIDER_OPTION_KEYS.has(key)) {
346
+ throw new Error(
347
+ `providerOptions cannot override protected chat field "${key}"; use the typed ChatOptions field instead`
348
+ );
349
+ }
350
+ }
351
+ return providerOptions;
352
+ }
315
353
  var TCloudClient = class _TCloudClient {
316
354
  baseURL;
317
355
  platformURL;
@@ -352,13 +390,21 @@ var TCloudClient = class _TCloudClient {
352
390
  * const reply = await client.ask('explain X', 'claude-code/sonnet')
353
391
  * ```
354
392
  *
355
- * For session-resumable agentic dispatches (file edits, multi-turn
356
- * coding), use the router-mediated `tcloud.bridge({...})` API instead
357
- * (or POST to cli-bridge directly with `session_id` in the body).
393
+ * For session-resumable agentic dispatches, use `client.bridge(...)` on
394
+ * the returned direct client. `resume` is serialized to cli-bridge's
395
+ * `session_id` body field and the model wire format stays
396
+ * `<harness>/<model>` without the router-only `bridge/` prefix.
358
397
  */
359
398
  static fromCliBridge(opts) {
360
399
  const baseURL = opts.url.replace(/\/+$/, "") + "/v1";
361
- return new _TCloudClient({ ...opts.config ?? {}, apiKey: opts.bearer, baseURL });
400
+ const client = new _TCloudClient({ ...opts.config ?? {}, apiKey: opts.bearer, baseURL });
401
+ Object.defineProperty(client, DIRECT_CLI_BRIDGE_MARKER, {
402
+ value: true,
403
+ enumerable: false,
404
+ configurable: false,
405
+ writable: false
406
+ });
407
+ return client;
362
408
  }
363
409
  /**
364
410
  * Build a client that rotates which operator serves each call. Mirrors
@@ -370,7 +416,7 @@ var TCloudClient = class _TCloudClient {
370
416
  *
371
417
  * ```ts
372
418
  * const tcloud = TCloudClient.rotating({
373
- * apiKey: process.env.TCLOUD_API_KEY,
419
+ * apiKey: process.env.TANGLE_API_KEY,
374
420
  * routing: { strategy: 'min-exposure' },
375
421
  * })
376
422
  * await tcloud.ask('hello')
@@ -411,7 +457,7 @@ var TCloudClient = class _TCloudClient {
411
457
  constructor(config = {}) {
412
458
  this.baseURL = (config.baseURL || DEFAULT_BASE_URL).replace(/\/$/, "");
413
459
  this.platformURL = (config.platformURL || DEFAULT_PLATFORM_URL).replace(/\/$/, "");
414
- this.apiKey = config.apiKey || process.env.TCLOUD_API_KEY;
460
+ this.apiKey = config.apiKey || process.env.TANGLE_API_KEY || process.env.TCLOUD_API_KEY;
415
461
  this.model = config.model || "gpt-4o-mini";
416
462
  this.privacy = config.privacy;
417
463
  this.limits = config.limits;
@@ -620,8 +666,8 @@ var TCloudClient = class _TCloudClient {
620
666
  delete headers["Authorization"];
621
667
  }
622
668
  }
623
- if (bridge) {
624
- headers["X-Bridge-Unlock"] = bridge.unlock;
669
+ if (bridge && !this._isDirectCliBridge()) {
670
+ headers["X-Bridge-Unlock"] = bridge.unlock ?? "";
625
671
  if (bridge.resume) headers["X-Resume"] = bridge.resume;
626
672
  if (bridge.bridgeUrl) headers["X-Bridge-Url"] = bridge.bridgeUrl;
627
673
  if (bridge.bridgeBearer) headers["X-Bridge-Bearer"] = bridge.bridgeBearer;
@@ -634,13 +680,24 @@ var TCloudClient = class _TCloudClient {
634
680
  */
635
681
  _effectiveModel(options) {
636
682
  if (options.bridge) {
683
+ if (this._isDirectCliBridge()) {
684
+ return options.bridge.model ? `${options.bridge.harness}/${options.bridge.model}` : options.bridge.harness;
685
+ }
637
686
  return options.bridge.model ? `bridge/${options.bridge.harness}/${options.bridge.model}` : `bridge/${options.bridge.harness}`;
638
687
  }
639
688
  return options.model || this.model;
640
689
  }
641
690
  /** Build the chat completions request body */
642
691
  _chatBody(options, stream) {
692
+ const providerOptions = sanitizeProviderOptions(options.providerOptions);
693
+ const sandboxBody = {};
694
+ if (options.sandbox?.agentProfile) sandboxBody.agent_profile = options.sandbox.agentProfile;
695
+ if (options.sandbox?.sessionId) sandboxBody.session_id = options.sandbox.sessionId;
696
+ if (this._isDirectCliBridge() && options.bridge?.resume && !sandboxBody.session_id) {
697
+ sandboxBody.session_id = options.bridge.resume;
698
+ }
643
699
  return JSON.stringify({
700
+ ...providerOptions,
644
701
  model: this._effectiveModel(options),
645
702
  messages: options.messages,
646
703
  temperature: options.temperature,
@@ -654,7 +711,7 @@ var TCloudClient = class _TCloudClient {
654
711
  tools: options.tools,
655
712
  tool_choice: options.toolChoice,
656
713
  ...options.gateway ? { gateway: options.gateway } : {},
657
- ...options.providerOptions
714
+ ...sandboxBody
658
715
  });
659
716
  }
660
717
  /** Chat completion (non-streaming) */
@@ -735,7 +792,13 @@ var TCloudClient = class _TCloudClient {
735
792
  "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."
736
793
  );
737
794
  }
738
- return new BridgeSession(this, cfg);
795
+ if (!this._isDirectCliBridge() && cfg.unlock == null) {
796
+ throw new Error("Bridge unlock is required for router-mediated bridge sessions");
797
+ }
798
+ return new BridgeSession(this, cfg, this._isDirectCliBridge());
799
+ }
800
+ _isDirectCliBridge() {
801
+ return this[DIRECT_CLI_BRIDGE_MARKER] === true;
739
802
  }
740
803
  /**
741
804
  * Rotation stats — populated only on clients created via
@@ -901,6 +964,49 @@ var TCloudClient = class _TCloudClient {
901
964
  })
902
965
  });
903
966
  }
967
+ /**
968
+ * Edit / inpaint / variate an existing image with a text prompt.
969
+ * Sibling to `imageGenerate`; routes to `/v1/images/edits` via
970
+ * multipart/form-data per the OpenAI spec.
971
+ *
972
+ * Reference image attachments may be passed as `Blob`, `ArrayBuffer`,
973
+ * or `{data: base64, mediaType, filename?}`. Multi-image composition
974
+ * (e.g. gpt-image-2 with two reference frames + a prompt that fuses
975
+ * them) is supported by passing an array; for legacy models only the
976
+ * first image is honored upstream.
977
+ */
978
+ async imagesEdit(options) {
979
+ const formData = new FormData();
980
+ formData.append("prompt", options.prompt);
981
+ formData.append("model", options.model || "gpt-image-2");
982
+ if (options.n != null) formData.append("n", String(options.n));
983
+ if (options.size) formData.append("size", options.size);
984
+ if (options.quality) formData.append("quality", options.quality);
985
+ if (options.response_format) formData.append("response_format", options.response_format);
986
+ if (options.mask) formData.append("mask", toEditBlob(options.mask, "mask.png"), "mask.png");
987
+ const images = Array.isArray(options.image) ? options.image : [options.image];
988
+ if (images.length === 0) {
989
+ throw new TCloudError(400, "imagesEdit requires at least one image attachment");
990
+ }
991
+ images.forEach((img, idx) => {
992
+ const blob = toEditBlob(img, `image-${idx + 1}.png`);
993
+ formData.append("image[]", blob, blobFilename(img, `image-${idx + 1}.png`));
994
+ });
995
+ const headers = { ...this.headers };
996
+ delete headers["Content-Type"];
997
+ this.checkLimits();
998
+ const res = await proxiedFetch(this.privacy, `${this.baseURL}/images/edits`, {
999
+ method: "POST",
1000
+ headers,
1001
+ body: formData
1002
+ }, false);
1003
+ if (!res.ok) {
1004
+ const err = await res.json().catch(() => ({ error: res.statusText }));
1005
+ throw new TCloudError(res.status, err.error?.message || err.error || err.message || res.statusText);
1006
+ }
1007
+ this._requestCount++;
1008
+ return res.json();
1009
+ }
904
1010
  /** Rerank documents by relevance to a query */
905
1011
  async rerank(options) {
906
1012
  return this._request(`${this.baseURL}/rerank`, {
@@ -992,7 +1098,7 @@ var TCloudClient = class _TCloudClient {
992
1098
  }
993
1099
  /** Get video generation status */
994
1100
  async videoStatus(id) {
995
- return this._fetch(`${this.baseURL}/video?id=${id}`);
1101
+ return this._fetch(`${this.baseURL}/video/${encodeURIComponent(id)}`);
996
1102
  }
997
1103
  /** Generate an avatar video (lip-synced talking head from audio + face image).
998
1104
  * Returns 202 with a job_id for async polling via avatarJobStatus(). */
@@ -1252,12 +1358,14 @@ var ALL_TIERS = [
1252
1358
  { name: "max-gpu-tee", cpu: 64, ramGb: 256, gpu: 4, tee: true }
1253
1359
  ];
1254
1360
  var BridgeSession = class _BridgeSession {
1255
- constructor(client, cfg) {
1361
+ constructor(client, cfg, direct = false) {
1256
1362
  this.client = client;
1257
1363
  this.cfg = cfg;
1364
+ this.direct = direct;
1258
1365
  }
1259
1366
  client;
1260
1367
  cfg;
1368
+ direct;
1261
1369
  /** Full chat completion (non-streaming). */
1262
1370
  async chat(options) {
1263
1371
  return this.client.chat({ ...options, bridge: this.cfg });
@@ -1291,15 +1399,16 @@ var BridgeSession = class _BridgeSession {
1291
1399
  }
1292
1400
  /** Clone with a new resume id — same harness, different logical conversation. */
1293
1401
  withResume(resume) {
1294
- return new _BridgeSession(this.client, { ...this.cfg, resume });
1402
+ return new _BridgeSession(this.client, { ...this.cfg, resume }, this.direct);
1295
1403
  }
1296
1404
  /** Clone with a different model inside the same harness. */
1297
1405
  withModel(model) {
1298
- return new _BridgeSession(this.client, { ...this.cfg, model });
1406
+ return new _BridgeSession(this.client, { ...this.cfg, model }, this.direct);
1299
1407
  }
1300
1408
  /** The effective model id that will land on the router (`bridge/<harness>/<model>`). */
1301
1409
  get model() {
1302
- return this.cfg.model ? `bridge/${this.cfg.harness}/${this.cfg.model}` : `bridge/${this.cfg.harness}`;
1410
+ const prefix = this.direct ? "" : "bridge/";
1411
+ return this.cfg.model ? `${prefix}${this.cfg.harness}/${this.cfg.model}` : `${prefix}${this.cfg.harness}`;
1303
1412
  }
1304
1413
  /** The resume id currently bound to this session, if any. */
1305
1414
  get resume() {
@@ -1321,6 +1430,32 @@ function selectTiers(all, n) {
1321
1430
  function formatPrice(pricePerToken) {
1322
1431
  return `$${(pricePerToken * 1e3).toFixed(6)}/1K tokens`;
1323
1432
  }
1433
+ function toEditBlob(input, defaultFilename) {
1434
+ if (input instanceof Blob) return input;
1435
+ if (input instanceof ArrayBuffer) return new Blob([input], { type: "image/png" });
1436
+ const binary = base64ToUint8Array(input.data);
1437
+ const copy = new Uint8Array(binary.byteLength);
1438
+ copy.set(binary);
1439
+ return new Blob([copy.buffer], { type: input.mediaType || "image/png" });
1440
+ void defaultFilename;
1441
+ }
1442
+ function blobFilename(input, fallback) {
1443
+ if (typeof input === "object" && !(input instanceof Blob) && !(input instanceof ArrayBuffer)) {
1444
+ if (input.filename) return input.filename;
1445
+ }
1446
+ return fallback;
1447
+ }
1448
+ function base64ToUint8Array(b64) {
1449
+ const m = /^data:[^;,]*(?:;[^,]*)?,(.+)$/.exec(b64);
1450
+ const raw = m ? m[1] : b64;
1451
+ const bin = typeof atob === "function" ? atob(raw) : (
1452
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1453
+ globalThis.Buffer.from(raw, "base64").toString("binary")
1454
+ );
1455
+ const out = new Uint8Array(bin.length);
1456
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
1457
+ return out;
1458
+ }
1324
1459
  var TCloudError = class extends Error {
1325
1460
  constructor(status, message) {
1326
1461
  super(message);
@@ -1,4 +1,5 @@
1
- import { a as TCloudConfig, T as TCloudClient } from './client-DkQugNHH.cjs';
1
+ import { a as TCloudConfig, T as TCloudClient } from './client-CF-tpVsi.cjs';
2
+ import '@tangle-network/sandbox';
2
3
 
3
4
  /**
4
5
  * Instance — programmatic harness for spinning up a local Tangle dev environment.
@@ -1,4 +1,5 @@
1
- import { a as TCloudConfig, T as TCloudClient } from './client-DkQugNHH.js';
1
+ import { a as TCloudConfig, T as TCloudClient } from './client-CF-tpVsi.js';
2
+ import '@tangle-network/sandbox';
2
3
 
3
4
  /**
4
5
  * Instance — programmatic harness for spinning up a local Tangle dev environment.
package/dist/instance.js CHANGED
@@ -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/instance.ts
6
6
  import { spawn } from "child_process";
package/dist/sandbox.cjs CHANGED
@@ -276,7 +276,14 @@ function buildSandboxCreateOptions(options) {
276
276
  memoryMB: options.memoryMb,
277
277
  diskGB: options.diskGb
278
278
  } : void 0,
279
- backend: options.backend ? { type: options.backend } : void 0,
279
+ // backend: merge `backend` (type string) with `agentProfile` (carried
280
+ // as `backend.profile` on the underlying SDK). Either may be set
281
+ // independently; setting both produces `{ type, profile }`. Setting
282
+ // neither leaves `backend` undefined so the SDK applies its default.
283
+ backend: options.backend || options.agentProfile ? {
284
+ ...options.backend ? { type: options.backend } : {},
285
+ ...options.agentProfile ? { profile: options.agentProfile } : {}
286
+ } : void 0,
280
287
  confidential: options.tee ? {
281
288
  tee: options.tee,
282
289
  sealed: options.sealed || void 0,
@@ -1,3 +1,4 @@
1
+ import { AgentProfile } from '@tangle-network/sandbox';
1
2
  import { AsyncAttestationPolicy, AttestationVerificationResult } from '@tangle-network/tcloud-attestation';
2
3
 
3
4
  type TCloudSandboxTee = 'any' | 'tdx' | 'nitro' | 'sev-snp' | 'phala-dstack' | 'gcp' | 'azure' | (string & {});
@@ -12,6 +13,25 @@ interface TCloudSandboxCreateOptions {
12
13
  gitUrl?: string;
13
14
  gitRef?: string;
14
15
  backend?: 'opencode' | 'claude-code' | 'codex' | 'amp' | (string & {});
16
+ /**
17
+ * Declarative agent profile rendered into `backend.profile` on the
18
+ * underlying `@tangle-network/sandbox` `CreateSandboxOptions`. The
19
+ * profile carries the harness's prompt, tool allowlist, MCP servers
20
+ * to attach, and subagents available for dispatch — everything a
21
+ * coding-agent backend needs to specialize.
22
+ *
23
+ * When set alongside `backend` (string), `backend.type` becomes the
24
+ * type and the profile rides as `backend.profile`. When `backend` is
25
+ * omitted, the SDK applies its default type and consumes the profile
26
+ * as-is.
27
+ *
28
+ * Confidential execution: if `agentProfile.confidential` is set AND
29
+ * `tee` is also requested at the top level, the top-level `tee`
30
+ * wins (it's the explicit op-level requirement). Set one or the
31
+ * other, not both. The SDK fails closed when the operator cannot
32
+ * satisfy the requested TEE.
33
+ */
34
+ agentProfile?: AgentProfile;
15
35
  tee?: TCloudSandboxTee;
16
36
  sealed?: boolean;
17
37
  attestationNonce?: string | 'auto';
package/dist/sandbox.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { AgentProfile } from '@tangle-network/sandbox';
1
2
  import { AsyncAttestationPolicy, AttestationVerificationResult } from '@tangle-network/tcloud-attestation';
2
3
 
3
4
  type TCloudSandboxTee = 'any' | 'tdx' | 'nitro' | 'sev-snp' | 'phala-dstack' | 'gcp' | 'azure' | (string & {});
@@ -12,6 +13,25 @@ interface TCloudSandboxCreateOptions {
12
13
  gitUrl?: string;
13
14
  gitRef?: string;
14
15
  backend?: 'opencode' | 'claude-code' | 'codex' | 'amp' | (string & {});
16
+ /**
17
+ * Declarative agent profile rendered into `backend.profile` on the
18
+ * underlying `@tangle-network/sandbox` `CreateSandboxOptions`. The
19
+ * profile carries the harness's prompt, tool allowlist, MCP servers
20
+ * to attach, and subagents available for dispatch — everything a
21
+ * coding-agent backend needs to specialize.
22
+ *
23
+ * When set alongside `backend` (string), `backend.type` becomes the
24
+ * type and the profile rides as `backend.profile`. When `backend` is
25
+ * omitted, the SDK applies its default type and consumes the profile
26
+ * as-is.
27
+ *
28
+ * Confidential execution: if `agentProfile.confidential` is set AND
29
+ * `tee` is also requested at the top level, the top-level `tee`
30
+ * wins (it's the explicit op-level requirement). Set one or the
31
+ * other, not both. The SDK fails closed when the operator cannot
32
+ * satisfy the requested TEE.
33
+ */
34
+ agentProfile?: AgentProfile;
15
35
  tee?: TCloudSandboxTee;
16
36
  sealed?: boolean;
17
37
  attestationNonce?: string | 'auto';
package/dist/sandbox.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  generateAttestationNonce,
6
6
  shouldVerifyAttestation,
7
7
  startTeeAttestationHeartbeat
8
- } from "./chunk-DBIT227N.js";
8
+ } from "./chunk-DRCOPW7D.js";
9
9
  export {
10
10
  TCloudSandbox,
11
11
  buildSandboxCreateOptions,
package/dist/shielded.cjs CHANGED
@@ -261,6 +261,7 @@ var PrivateRouter = class {
261
261
 
262
262
  // src/client.ts
263
263
  var ROTATING_MARKER = "__tcloudRotating";
264
+ var DIRECT_CLI_BRIDGE_MARKER = "__tcloudDirectCliBridge";
264
265
  var DEFAULT_BASE_URL = "https://router.tangle.tools/v1";
265
266
  var SDK_VERSION = "0.4.0";
266
267
  async function proxiedFetch(privacy, url, init, streaming) {
@@ -310,6 +311,43 @@ var DEFAULT_RETRY = {
310
311
  };
311
312
  var DEFAULT_TIMEOUT_MS = 6e4;
312
313
  var DEFAULT_PLATFORM_URL = "https://id.tangle.tools";
314
+ var PROTECTED_PROVIDER_OPTION_KEYS = /* @__PURE__ */ new Set([
315
+ "model",
316
+ "messages",
317
+ "temperature",
318
+ "max_tokens",
319
+ "maxTokens",
320
+ "stream",
321
+ "stop",
322
+ "top_p",
323
+ "topP",
324
+ "frequency_penalty",
325
+ "frequencyPenalty",
326
+ "presence_penalty",
327
+ "presencePenalty",
328
+ "response_format",
329
+ "responseFormat",
330
+ "tools",
331
+ "tool_choice",
332
+ "toolChoice",
333
+ "gateway",
334
+ "bridge",
335
+ "agent_profile",
336
+ "agentProfile",
337
+ "session_id",
338
+ "sessionId"
339
+ ]);
340
+ function sanitizeProviderOptions(providerOptions) {
341
+ if (!providerOptions) return {};
342
+ for (const key of Object.keys(providerOptions)) {
343
+ if (PROTECTED_PROVIDER_OPTION_KEYS.has(key)) {
344
+ throw new Error(
345
+ `providerOptions cannot override protected chat field "${key}"; use the typed ChatOptions field instead`
346
+ );
347
+ }
348
+ }
349
+ return providerOptions;
350
+ }
313
351
  var TCloudClient = class _TCloudClient {
314
352
  baseURL;
315
353
  platformURL;
@@ -350,13 +388,21 @@ var TCloudClient = class _TCloudClient {
350
388
  * const reply = await client.ask('explain X', 'claude-code/sonnet')
351
389
  * ```
352
390
  *
353
- * For session-resumable agentic dispatches (file edits, multi-turn
354
- * coding), use the router-mediated `tcloud.bridge({...})` API instead
355
- * (or POST to cli-bridge directly with `session_id` in the body).
391
+ * For session-resumable agentic dispatches, use `client.bridge(...)` on
392
+ * the returned direct client. `resume` is serialized to cli-bridge's
393
+ * `session_id` body field and the model wire format stays
394
+ * `<harness>/<model>` without the router-only `bridge/` prefix.
356
395
  */
357
396
  static fromCliBridge(opts) {
358
397
  const baseURL = opts.url.replace(/\/+$/, "") + "/v1";
359
- return new _TCloudClient({ ...opts.config ?? {}, apiKey: opts.bearer, baseURL });
398
+ const client = new _TCloudClient({ ...opts.config ?? {}, apiKey: opts.bearer, baseURL });
399
+ Object.defineProperty(client, DIRECT_CLI_BRIDGE_MARKER, {
400
+ value: true,
401
+ enumerable: false,
402
+ configurable: false,
403
+ writable: false
404
+ });
405
+ return client;
360
406
  }
361
407
  /**
362
408
  * Build a client that rotates which operator serves each call. Mirrors
@@ -368,7 +414,7 @@ var TCloudClient = class _TCloudClient {
368
414
  *
369
415
  * ```ts
370
416
  * const tcloud = TCloudClient.rotating({
371
- * apiKey: process.env.TCLOUD_API_KEY,
417
+ * apiKey: process.env.TANGLE_API_KEY,
372
418
  * routing: { strategy: 'min-exposure' },
373
419
  * })
374
420
  * await tcloud.ask('hello')
@@ -409,7 +455,7 @@ var TCloudClient = class _TCloudClient {
409
455
  constructor(config = {}) {
410
456
  this.baseURL = (config.baseURL || DEFAULT_BASE_URL).replace(/\/$/, "");
411
457
  this.platformURL = (config.platformURL || DEFAULT_PLATFORM_URL).replace(/\/$/, "");
412
- this.apiKey = config.apiKey || process.env.TCLOUD_API_KEY;
458
+ this.apiKey = config.apiKey || process.env.TANGLE_API_KEY || process.env.TCLOUD_API_KEY;
413
459
  this.model = config.model || "gpt-4o-mini";
414
460
  this.privacy = config.privacy;
415
461
  this.limits = config.limits;
@@ -618,8 +664,8 @@ var TCloudClient = class _TCloudClient {
618
664
  delete headers["Authorization"];
619
665
  }
620
666
  }
621
- if (bridge) {
622
- headers["X-Bridge-Unlock"] = bridge.unlock;
667
+ if (bridge && !this._isDirectCliBridge()) {
668
+ headers["X-Bridge-Unlock"] = bridge.unlock ?? "";
623
669
  if (bridge.resume) headers["X-Resume"] = bridge.resume;
624
670
  if (bridge.bridgeUrl) headers["X-Bridge-Url"] = bridge.bridgeUrl;
625
671
  if (bridge.bridgeBearer) headers["X-Bridge-Bearer"] = bridge.bridgeBearer;
@@ -632,13 +678,24 @@ var TCloudClient = class _TCloudClient {
632
678
  */
633
679
  _effectiveModel(options) {
634
680
  if (options.bridge) {
681
+ if (this._isDirectCliBridge()) {
682
+ return options.bridge.model ? `${options.bridge.harness}/${options.bridge.model}` : options.bridge.harness;
683
+ }
635
684
  return options.bridge.model ? `bridge/${options.bridge.harness}/${options.bridge.model}` : `bridge/${options.bridge.harness}`;
636
685
  }
637
686
  return options.model || this.model;
638
687
  }
639
688
  /** Build the chat completions request body */
640
689
  _chatBody(options, stream) {
690
+ const providerOptions = sanitizeProviderOptions(options.providerOptions);
691
+ const sandboxBody = {};
692
+ if (options.sandbox?.agentProfile) sandboxBody.agent_profile = options.sandbox.agentProfile;
693
+ if (options.sandbox?.sessionId) sandboxBody.session_id = options.sandbox.sessionId;
694
+ if (this._isDirectCliBridge() && options.bridge?.resume && !sandboxBody.session_id) {
695
+ sandboxBody.session_id = options.bridge.resume;
696
+ }
641
697
  return JSON.stringify({
698
+ ...providerOptions,
642
699
  model: this._effectiveModel(options),
643
700
  messages: options.messages,
644
701
  temperature: options.temperature,
@@ -652,7 +709,7 @@ var TCloudClient = class _TCloudClient {
652
709
  tools: options.tools,
653
710
  tool_choice: options.toolChoice,
654
711
  ...options.gateway ? { gateway: options.gateway } : {},
655
- ...options.providerOptions
712
+ ...sandboxBody
656
713
  });
657
714
  }
658
715
  /** Chat completion (non-streaming) */
@@ -733,7 +790,13 @@ var TCloudClient = class _TCloudClient {
733
790
  "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."
734
791
  );
735
792
  }
736
- return new BridgeSession(this, cfg);
793
+ if (!this._isDirectCliBridge() && cfg.unlock == null) {
794
+ throw new Error("Bridge unlock is required for router-mediated bridge sessions");
795
+ }
796
+ return new BridgeSession(this, cfg, this._isDirectCliBridge());
797
+ }
798
+ _isDirectCliBridge() {
799
+ return this[DIRECT_CLI_BRIDGE_MARKER] === true;
737
800
  }
738
801
  /**
739
802
  * Rotation stats — populated only on clients created via
@@ -899,6 +962,49 @@ var TCloudClient = class _TCloudClient {
899
962
  })
900
963
  });
901
964
  }
965
+ /**
966
+ * Edit / inpaint / variate an existing image with a text prompt.
967
+ * Sibling to `imageGenerate`; routes to `/v1/images/edits` via
968
+ * multipart/form-data per the OpenAI spec.
969
+ *
970
+ * Reference image attachments may be passed as `Blob`, `ArrayBuffer`,
971
+ * or `{data: base64, mediaType, filename?}`. Multi-image composition
972
+ * (e.g. gpt-image-2 with two reference frames + a prompt that fuses
973
+ * them) is supported by passing an array; for legacy models only the
974
+ * first image is honored upstream.
975
+ */
976
+ async imagesEdit(options) {
977
+ const formData = new FormData();
978
+ formData.append("prompt", options.prompt);
979
+ formData.append("model", options.model || "gpt-image-2");
980
+ if (options.n != null) formData.append("n", String(options.n));
981
+ if (options.size) formData.append("size", options.size);
982
+ if (options.quality) formData.append("quality", options.quality);
983
+ if (options.response_format) formData.append("response_format", options.response_format);
984
+ if (options.mask) formData.append("mask", toEditBlob(options.mask, "mask.png"), "mask.png");
985
+ const images = Array.isArray(options.image) ? options.image : [options.image];
986
+ if (images.length === 0) {
987
+ throw new TCloudError(400, "imagesEdit requires at least one image attachment");
988
+ }
989
+ images.forEach((img, idx) => {
990
+ const blob = toEditBlob(img, `image-${idx + 1}.png`);
991
+ formData.append("image[]", blob, blobFilename(img, `image-${idx + 1}.png`));
992
+ });
993
+ const headers = { ...this.headers };
994
+ delete headers["Content-Type"];
995
+ this.checkLimits();
996
+ const res = await proxiedFetch(this.privacy, `${this.baseURL}/images/edits`, {
997
+ method: "POST",
998
+ headers,
999
+ body: formData
1000
+ }, false);
1001
+ if (!res.ok) {
1002
+ const err = await res.json().catch(() => ({ error: res.statusText }));
1003
+ throw new TCloudError(res.status, err.error?.message || err.error || err.message || res.statusText);
1004
+ }
1005
+ this._requestCount++;
1006
+ return res.json();
1007
+ }
902
1008
  /** Rerank documents by relevance to a query */
903
1009
  async rerank(options) {
904
1010
  return this._request(`${this.baseURL}/rerank`, {
@@ -990,7 +1096,7 @@ var TCloudClient = class _TCloudClient {
990
1096
  }
991
1097
  /** Get video generation status */
992
1098
  async videoStatus(id) {
993
- return this._fetch(`${this.baseURL}/video?id=${id}`);
1099
+ return this._fetch(`${this.baseURL}/video/${encodeURIComponent(id)}`);
994
1100
  }
995
1101
  /** Generate an avatar video (lip-synced talking head from audio + face image).
996
1102
  * Returns 202 with a job_id for async polling via avatarJobStatus(). */
@@ -1250,12 +1356,14 @@ var ALL_TIERS = [
1250
1356
  { name: "max-gpu-tee", cpu: 64, ramGb: 256, gpu: 4, tee: true }
1251
1357
  ];
1252
1358
  var BridgeSession = class _BridgeSession {
1253
- constructor(client, cfg) {
1359
+ constructor(client, cfg, direct = false) {
1254
1360
  this.client = client;
1255
1361
  this.cfg = cfg;
1362
+ this.direct = direct;
1256
1363
  }
1257
1364
  client;
1258
1365
  cfg;
1366
+ direct;
1259
1367
  /** Full chat completion (non-streaming). */
1260
1368
  async chat(options) {
1261
1369
  return this.client.chat({ ...options, bridge: this.cfg });
@@ -1289,15 +1397,16 @@ var BridgeSession = class _BridgeSession {
1289
1397
  }
1290
1398
  /** Clone with a new resume id — same harness, different logical conversation. */
1291
1399
  withResume(resume) {
1292
- return new _BridgeSession(this.client, { ...this.cfg, resume });
1400
+ return new _BridgeSession(this.client, { ...this.cfg, resume }, this.direct);
1293
1401
  }
1294
1402
  /** Clone with a different model inside the same harness. */
1295
1403
  withModel(model) {
1296
- return new _BridgeSession(this.client, { ...this.cfg, model });
1404
+ return new _BridgeSession(this.client, { ...this.cfg, model }, this.direct);
1297
1405
  }
1298
1406
  /** The effective model id that will land on the router (`bridge/<harness>/<model>`). */
1299
1407
  get model() {
1300
- return this.cfg.model ? `bridge/${this.cfg.harness}/${this.cfg.model}` : `bridge/${this.cfg.harness}`;
1408
+ const prefix = this.direct ? "" : "bridge/";
1409
+ return this.cfg.model ? `${prefix}${this.cfg.harness}/${this.cfg.model}` : `${prefix}${this.cfg.harness}`;
1301
1410
  }
1302
1411
  /** The resume id currently bound to this session, if any. */
1303
1412
  get resume() {
@@ -1319,6 +1428,32 @@ function selectTiers(all, n) {
1319
1428
  function formatPrice(pricePerToken) {
1320
1429
  return `$${(pricePerToken * 1e3).toFixed(6)}/1K tokens`;
1321
1430
  }
1431
+ function toEditBlob(input, defaultFilename) {
1432
+ if (input instanceof Blob) return input;
1433
+ if (input instanceof ArrayBuffer) return new Blob([input], { type: "image/png" });
1434
+ const binary = base64ToUint8Array(input.data);
1435
+ const copy = new Uint8Array(binary.byteLength);
1436
+ copy.set(binary);
1437
+ return new Blob([copy.buffer], { type: input.mediaType || "image/png" });
1438
+ void defaultFilename;
1439
+ }
1440
+ function blobFilename(input, fallback) {
1441
+ if (typeof input === "object" && !(input instanceof Blob) && !(input instanceof ArrayBuffer)) {
1442
+ if (input.filename) return input.filename;
1443
+ }
1444
+ return fallback;
1445
+ }
1446
+ function base64ToUint8Array(b64) {
1447
+ const m = /^data:[^;,]*(?:;[^,]*)?,(.+)$/.exec(b64);
1448
+ const raw = m ? m[1] : b64;
1449
+ const bin = typeof atob === "function" ? atob(raw) : (
1450
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1451
+ globalThis.Buffer.from(raw, "base64").toString("binary")
1452
+ );
1453
+ const out = new Uint8Array(bin.length);
1454
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
1455
+ return out;
1456
+ }
1322
1457
  var TCloudError = class extends Error {
1323
1458
  constructor(status, message) {
1324
1459
  super(message);