@tangle-network/agent-provider-cli-bridge 2.0.2 → 2.0.4

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
@@ -104,6 +104,21 @@ The provider uses the explicit SSE event field to select the format and rejects
104
104
  The provider rejects a terminal response when its run id or request digest does not match the accepted stream.
105
105
  Usage events and terminal result metadata include `modelRequests` only when the bridge measured an exact non-negative integer count.
106
106
  Missing, malformed, partial, or estimated request counts remain unknown.
107
+
108
+ `metadata.model` and event `data.model` retain Bridge's compatibility response value; they do not prove which model served a request.
109
+ Explicit provider-response observations appear as `provider_models` in event data and result metadata.
110
+ `provider_model` is present only for one distinct observation, and never on a failed result.
111
+ The collector retains disagreements independently of its bounded result event history; overflow or malformed evidence refuses certification.
112
+ Requested routes, init records, fingerprints, configured session models and application JSON never establish provider identity.
113
+ Opaque native records remain in their existing envelope and cannot promote their own fields into host evidence.
114
+
115
+ On the qualified OpenAI wire path, raw Bridge usage is retained in `event.providerEvent.usage` even when a partial receipt cannot form `TokenUsage`.
116
+ `metadata.provider_usage` folds those receipts outside the bounded event history: a missing count stays unknown across later deltas, and a cumulative receipt replaces counts and coverage.
117
+ Events declare this distinction with the existing `usageMode` field.
118
+ Measured usage requires explicit `estimated: false` and valid reported input and output totals; `estimated: true` is an estimate, and absent provenance remains unknown.
119
+ Observed zero is retained. Missing cache classes and cost remain unknown; `cost_scope` applies only to cost.
120
+ The numeric `result.usage` compatibility projection alone does not certify complete measured usage.
121
+ Old peers and opaque canonical native envelopes remain readable with uncertified provenance; nested native or application records cannot assert host observation authority.
107
122
  When `dispatch()` receives no `sessionId`, it creates one from that turn's stable run id and returns it.
108
123
  Replay and result reads fail loudly after cli-bridge's configured replay retention expires.
109
124
  Stopping a `session.events()` reader detaches only that replay observer.
@@ -180,3 +195,21 @@ Those five surfaces are never claimed in the capability document and always carr
180
195
  The usage of one run is summed across its events and attributed to that run's execution id, which the observation carries as the provenance source.
181
196
  An environment rebuilt with `provider.get(id)` has measured nothing yet, so it reports the usage surface as unavailable until a turn completes through it.
182
197
  The endpoint holds only a scheme, a host, and an explicit port; userinfo, path, and query are dropped, and the bearer beside the base URL is never read.
198
+
199
+ ## Cloud sandboxes (`./sandbox`)
200
+
201
+ `@tangle-network/agent-provider-cli-bridge/sandbox` turns a vendor sandbox that runs CLI Bridge into an `AgentEnvironmentProvider`.
202
+ The vendor adapter implements `BridgeSandboxVendor`: `create`, `connect`, `list`, and per sandbox `exec`, `spawn`, `read`, `write`, `endpoint`, `status` and `destroy`.
203
+ `createBridgeSandboxProvider({ vendor, router: { key } })` does the rest:
204
+
205
+ - It refuses every create field the vendor cannot honor with an `EnvironmentProviderError` that names the field, before any vendor call, and validates the profile with the materializer the Bridge in the box uses.
206
+ - It creates the sandbox from the box image with a finite lifetime and a run tag, starts the Bridge with a random bearer and the per-run Router key on that process only, waits for the selected backend to report ready, checks the harness CLI against the registry's pin, clones `workspace.repoUrl`, and creates the environment through `createCliBridgeProvider` at the exposed URL.
207
+ - Any failure after the vendor create destroys the sandbox before the error surfaces; a destroy that fails carries the sandbox id as `cleanupHandle`.
208
+ - Turns run through the Bridge. `turn.timeoutMs` cancels the run and ends the stream with a failed `result` that names the timeout, an abort rejects with `AbortError`, and a second turn on a busy environment is refused with `conflict`.
209
+ - Events carry `execution.started` with the declared model and a terminal `result` with the model the harness reported serving. The Bridge's dollar figure is the harness's catalog estimate, so events omit `usage.cost`.
210
+
211
+ The box image is `renderBoxDockerfile()`: Node, CLI Bridge at the commit in `BOX_PINS`, and each harness CLI at the version `@tangle-network/cli-agent-registry` pins.
212
+ A harness joins `BOX_HARNESSES` only after a live turn through the Router completed in a box; `claude-code` is the proven row.
213
+ Build the image for a cloud with `pnpm build:sandbox-image --target e2b|daytona|docker` from the repository root.
214
+ `@tangle-network/agent-provider-testkit` runs the live profile conformance suite against the result.
215
+
@@ -1,6 +1,6 @@
1
1
  import { type StreamEvent } from "@tangle-network/agent-interface";
2
2
  import type { AgentEnvironmentEvent } from "@tangle-network/agent-interface/environment-provider";
3
- import type { CliBridgeSseFrame } from "./wire.js";
3
+ import { type CliBridgeSseFrame } from "./wire.js";
4
4
  interface CanonicalEventContext {
5
5
  readonly runId: string;
6
6
  readonly sessionId: string;
@@ -1,4 +1,5 @@
1
1
  import { RuntimeEventEnvelopeSchema, } from "@tangle-network/agent-interface";
2
+ import { providerUsageFromOpenAi as recordValue, usageFromOpenAi } from "./wire.js";
2
3
  /** Decode one explicitly typed retained-run frame without guessing its wire format. */
3
4
  export function retainedCanonicalEvent(frame, context) {
4
5
  if (frame.event === undefined)
@@ -82,39 +83,12 @@ function usageFromCanonicalEvent(event) {
82
83
  const usage = recordValue(providerEvent.usage) ?? recordValue(data?.usage);
83
84
  if (!usage)
84
85
  return undefined;
85
- const inputTokens = nonNegativeInteger(usage.inputTokens);
86
- const outputTokens = nonNegativeInteger(usage.outputTokens);
87
- if (inputTokens === undefined || outputTokens === undefined)
88
- return undefined;
89
- return {
90
- inputTokens,
91
- outputTokens,
92
- ...optionalInteger(usage, "totalTokens"),
93
- ...optionalInteger(usage, "cacheReadInputTokens"),
94
- ...optionalInteger(usage, "cacheCreationInputTokens"),
95
- ...optionalInteger(usage, "reasoningTokens"),
96
- ...optionalNumber(usage, "cost"),
97
- };
98
- }
99
- function optionalInteger(source, key) {
100
- const value = nonNegativeInteger(source[key]);
101
- return value === undefined ? {} : { [key]: value };
102
- }
103
- function optionalNumber(source, key) {
104
- const value = source[key];
105
- return typeof value === "number" && Number.isFinite(value) && value >= 0
106
- ? { [key]: value }
107
- : {};
108
- }
109
- function nonNegativeInteger(value) {
110
- return typeof value === "number" && Number.isSafeInteger(value) && value >= 0
111
- ? value
112
- : undefined;
113
- }
114
- function recordValue(value) {
115
- return typeof value === "object" && value !== null && !Array.isArray(value)
116
- ? value
117
- : undefined;
86
+ return usageFromOpenAi({
87
+ prompt_tokens: usage.inputTokens, completion_tokens: usage.outputTokens,
88
+ total_tokens: usage.totalTokens, cache_read_input_tokens: usage.cacheReadInputTokens,
89
+ cache_write_input_tokens: usage.cacheCreationInputTokens, reasoning_tokens: usage.reasoningTokens,
90
+ cost: usage.cost,
91
+ });
118
92
  }
119
93
  function isBaseTenInteger(value) {
120
94
  return /^(0|[1-9][0-9]*)$/u.test(value) && Number.isSafeInteger(Number(value));
@@ -46,11 +46,10 @@ export function createCliBridgeEnvironment(args) {
46
46
  }
47
47
  assertCliBridgeRequestedInteractions(args.capabilities, turn.interactions);
48
48
  const prepared = prepareCliBridgeRun(options, environmentInput, turn, providerName, environmentId, useNativeInteractions, args.acceptedContextTransfer);
49
- // A streamed run reports usage across its events, so the totals are summed
50
- // here and recorded as one execution's measurement.
49
+ // Delta receipts add; cumulative receipts replace this execution's measurement.
51
50
  let streamedUsage;
52
51
  for await (const event of streamTrackedCliBridgeTurn(options, environmentInput, providerName, prepared, transport, runs, sessions, readers, nativeSessions, useNativeInteractions)) {
53
- streamedUsage = addTokenUsage(streamedUsage, event.usage);
52
+ streamedUsage = addTokenUsage(streamedUsage, event.usage, event.usageMode);
54
53
  usageLog.record(prepared.run.executionId, streamedUsage);
55
54
  yield event;
56
55
  }
@@ -14,5 +14,5 @@ export declare function streamCliBridgeSessionEvents(options: CliBridgeProviderO
14
14
  export declare function collectCliBridgeTurnResult(source: AsyncIterable<AgentEnvironmentEvent>, run: CliBridgeRun, options: CliBridgeProviderOptions, transport: CliBridgeTransport, signal?: AbortSignal): Promise<AgentTurnResult>;
15
15
  /** Keep reasoning and tool updates out of the assistant's visible result text. */
16
16
  export declare function visibleTextDelta(event: AgentEnvironmentEvent): string | undefined;
17
- /** Sum the usage a run reported across its events into one total. */
18
- export declare function addTokenUsage(current: TokenUsage | undefined, next: TokenUsage | undefined): TokenUsage | undefined;
17
+ /** Respect cumulative receipts and keep missing optional classes unknown. */
18
+ export declare function addTokenUsage(current: TokenUsage | undefined, next: TokenUsage | undefined, mode?: "delta" | "cumulative"): TokenUsage | undefined;
@@ -1,10 +1,9 @@
1
- import { AgentTurnResultSchema, } from "@tangle-network/agent-interface";
1
+ import { AgentTurnResultSchema, CONTRACT_MAX_ARRAY_LENGTH, } from "@tangle-network/agent-interface";
2
2
  import { cancelCliBridgeRun, captureCliBridgeRunIdentity, cliBridgeCancellationSignal, detachCliBridgeReader, getCliBridgeRun, } from "./retained-control.js";
3
3
  import { bindCliBridgeRun, bindCliBridgeSession, exactControlRefForRun, restoreCliBridgeRun, restoreCliBridgeSession, } from "./retained-run-state.js";
4
- import { CliBridgeRequestRejectedError, streamCliBridgeTurn, } from "./retained-stream.js";
4
+ import { CliBridgeRequestRejectedError, openCliBridgeStream, streamCliBridgeTurn, } from "./retained-stream.js";
5
5
  import { beginCliBridgeNativeTurn, } from "./retained-native.js";
6
- import { MAX_CLI_BRIDGE_CONTROL_RESPONSE_BYTES, readBoundedCliBridgeResponse, requestHeaders, trimSlash, } from "./transport.js";
7
- import { modelRequestCount } from "./wire.js";
6
+ import { modelRequestCount, providerUsageFromOpenAi, responseIdentityFromOpenAi, usageFromOpenAi } from "./wire.js";
8
7
  import { lookupCliBridgeContext } from "./context-transfer.js";
9
8
  const MAX_CLI_BRIDGE_RESULT_EVENTS = 1_024;
10
9
  export async function* streamTrackedCliBridgeTurn(options, environmentInput, providerName, prepared, transport, runs, sessions, readers, nativeSessions, useNativeInteractions) {
@@ -119,26 +118,9 @@ export async function dispatchCliBridgeTurn(options, environmentInput, prepared,
119
118
  });
120
119
  }
121
120
  else {
122
- const response = await transport.fetch(`${trimSlash(options.baseUrl)}/v1/chat/completions`, {
123
- method: "POST",
124
- headers: {
125
- ...requestHeaders(options),
126
- accept: "text/event-stream",
127
- ...(run.sessionId ? { "x-session-id": run.sessionId } : {}),
128
- },
129
- body: run.requestBody,
130
- ...(signal ? { signal } : {}),
131
- });
132
- if (!response.ok) {
133
- let detail = "request rejected";
134
- try {
135
- detail = await readBoundedCliBridgeResponse(response, MAX_CLI_BRIDGE_CONTROL_RESPONSE_BYTES, signal);
136
- }
137
- catch {
138
- // The HTTP status already proves this request was rejected.
139
- }
140
- throw new CliBridgeRequestRejectedError(response.status, detail);
141
- }
121
+ const response = await openCliBridgeStream([
122
+ options, { ...prepared.turn, sessionId: run.sessionId }, run.requestBody, transport, run.id, undefined, signal,
123
+ ]);
142
124
  accepted = true;
143
125
  if (!response.body)
144
126
  throw new Error("cli-bridge response body is empty");
@@ -261,6 +243,9 @@ export async function collectCliBridgeTurnResult(source, run, options, transport
261
243
  let eventCount = 0;
262
244
  let text = "";
263
245
  let usage;
246
+ let providerUsage;
247
+ let providerTokensSeen = false;
248
+ const providerModels = new Set();
264
249
  let observedModelRequests = 0;
265
250
  let modelRequestsKnown = false;
266
251
  let terminalModelRequests;
@@ -283,7 +268,20 @@ export async function collectCliBridgeTurnResult(source, run, options, transport
283
268
  if (delta !== undefined)
284
269
  text += delta;
285
270
  }
286
- usage = addTokenUsage(usage, event.usage);
271
+ const observedUsage = providerUsageFromOpenAi(providerUsageFromOpenAi(event.providerEvent)?.usage);
272
+ const rawUsage = observedUsage ??
273
+ (event.usage ? { prompt_tokens: event.usage.inputTokens, completion_tokens: event.usage.outputTokens } : undefined);
274
+ if (rawUsage) {
275
+ providerUsage = foldProviderUsage(providerUsage, rawUsage, event.usageMode, providerTokensSeen);
276
+ providerTokensSeen = event.usageMode === "cumulative"
277
+ ? hasTokenReceipt(rawUsage) : providerTokensSeen || hasTokenReceipt(rawUsage);
278
+ }
279
+ usage = observedUsage ? usageFromOpenAi(providerUsage) : addTokenUsage(usage, event.usage, event.usageMode);
280
+ for (const model of responseIdentityFromOpenAi(event.data)?.provider_models ?? []) {
281
+ providerModels.add(model);
282
+ if (providerModels.size > CONTRACT_MAX_ARRAY_LENGTH)
283
+ throw new Error("cli-bridge provider model observations exceed contract bounds");
284
+ }
287
285
  const eventModel = event.data.model;
288
286
  const eventFingerprint = event.data.system_fingerprint;
289
287
  if (typeof eventModel === "string" && eventModel.length > 0)
@@ -350,6 +348,9 @@ export async function collectCliBridgeTurnResult(source, run, options, transport
350
348
  ? { modelRequests: observedModelRequests }
351
349
  : {}),
352
350
  ...(servedModel === undefined ? {} : { model: servedModel }),
351
+ ...(providerModels.size ? { provider_models: [...providerModels] } : {}),
352
+ ...(success && providerModels.size === 1 ? { provider_model: [...providerModels][0] } : {}),
353
+ ...(providerUsage === undefined ? {} : { provider_usage: providerUsage }),
353
354
  ...(systemFingerprint === undefined ? {} : { system_fingerprint: systemFingerprint }),
354
355
  },
355
356
  events: resultEvents,
@@ -374,39 +375,56 @@ export function visibleTextDelta(event) {
374
375
  }
375
376
  return typeof event.data.delta === "string" ? event.data.delta : undefined;
376
377
  }
377
- /** Sum the usage a run reported across its events into one total. */
378
- export function addTokenUsage(current, next) {
378
+ /** Respect cumulative receipts and keep missing optional classes unknown. */
379
+ export function addTokenUsage(current, next, mode) {
380
+ if (mode === "cumulative")
381
+ return next ? { ...next } : undefined;
379
382
  if (!next)
380
383
  return current;
381
384
  if (!current)
382
385
  return { ...next };
383
- return {
384
- inputTokens: current.inputTokens + next.inputTokens,
385
- outputTokens: current.outputTokens + next.outputTokens,
386
- ...(current.totalTokens !== undefined || next.totalTokens !== undefined
387
- ? { totalTokens: (current.totalTokens ?? 0) + (next.totalTokens ?? 0) }
388
- : {}),
389
- ...(current.cacheReadInputTokens !== undefined ||
390
- next.cacheReadInputTokens !== undefined
391
- ? {
392
- cacheReadInputTokens: (current.cacheReadInputTokens ?? 0) +
393
- (next.cacheReadInputTokens ?? 0),
394
- }
395
- : {}),
396
- ...(current.cacheCreationInputTokens !== undefined ||
397
- next.cacheCreationInputTokens !== undefined
398
- ? {
399
- cacheCreationInputTokens: (current.cacheCreationInputTokens ?? 0) +
400
- (next.cacheCreationInputTokens ?? 0),
401
- }
402
- : {}),
403
- ...(current.reasoningTokens !== undefined || next.reasoningTokens !== undefined
404
- ? {
405
- reasoningTokens: (current.reasoningTokens ?? 0) + (next.reasoningTokens ?? 0),
386
+ return Object.fromEntries(Object.entries(next).flatMap(([key, count]) => {
387
+ const prior = current[key];
388
+ return prior === undefined ? [] : [[key, prior + count]];
389
+ }));
390
+ }
391
+ const TOKEN_FIELDS = ["prompt_tokens", "input_tokens", "completion_tokens", "output_tokens", "total_tokens",
392
+ "fresh_input_tokens", "cache_read_input_tokens", "cache_write_input_tokens", "cache_creation_input_tokens", "reasoning_tokens"];
393
+ const hasTokenReceipt = (usage) => TOKEN_FIELDS.some(key => key in usage) || "estimated" in usage;
394
+ /** Coverage lives outside the bounded event ring: a later delta cannot fill an earlier omission. */
395
+ function foldProviderUsage(current, next, mode, hasPriorTokens) {
396
+ if (!current || mode === "cumulative")
397
+ return { ...next };
398
+ const folded = { ...current, ...next };
399
+ if (hasTokenReceipt(next)) {
400
+ for (const key of [...TOKEN_FIELDS, "model_requests"]) {
401
+ const value = next[key], prior = current[key];
402
+ if (typeof value === "number" && Number.isSafeInteger(value) && value >= 0 &&
403
+ (!hasPriorTokens || (typeof prior === "number" && Number.isSafeInteger(prior) && prior >= 0))) {
404
+ const count = value + (hasPriorTokens ? prior : 0);
405
+ if (Number.isSafeInteger(count)) {
406
+ folded[key] = count;
407
+ continue;
408
+ }
406
409
  }
407
- : {}),
408
- ...(current.cost !== undefined || next.cost !== undefined
409
- ? { cost: (current.cost ?? 0) + (next.cost ?? 0) }
410
- : {}),
411
- };
410
+ delete folded[key];
411
+ }
412
+ const estimated = current.estimated === true || next.estimated === true ? true :
413
+ next.estimated === false && (!hasPriorTokens || current.estimated === false) ? false : undefined;
414
+ if (estimated === undefined)
415
+ delete folded.estimated;
416
+ else
417
+ folded.estimated = estimated;
418
+ }
419
+ if (next.cost_scope !== "total") {
420
+ const cost = current.cost, delta = next.cost;
421
+ if (current.cost_known === true && next.cost_known === true && typeof cost === "number" && typeof delta === "number") {
422
+ folded.cost = cost + delta;
423
+ }
424
+ else {
425
+ delete folded.cost;
426
+ folded.cost_known = false;
427
+ }
428
+ }
429
+ return folded;
412
430
  }
@@ -7,3 +7,5 @@ export declare class CliBridgeRequestRejectedError extends Error {
7
7
  constructor(status: number, detail: string);
8
8
  }
9
9
  export declare function streamCliBridgeTurn(options: CliBridgeProviderOptions, turn: AgentTurnInput, requestBody: string | undefined, transport: CliBridgeTransport, runId: string, lastEventId?: string, signal?: AbortSignal, onResponse?: (response: CliBridgeResponse) => void, readAuthoritativeRun?: () => Promise<CliBridgeRunSnapshot | null>, validateCanonicalEvent?: (event: AgentEnvironmentEvent) => void): AsyncIterable<AgentEnvironmentEvent>;
10
+ /** Both dispatch and live readers use the same admission and bounded HTTP refusal. */
11
+ export declare function openCliBridgeStream([options, turn, requestBody, transport, runId, lastEventId, signal, onResponse]: Parameters<typeof streamCliBridgeTurn>): Promise<CliBridgeResponse>;
@@ -2,7 +2,7 @@ import { retainedEventsWithIdentity, retainedReplayRequest, } from "./retained-e
2
2
  import { isTerminalCanonicalEvent, retainedCanonicalEvent, } from "./retained-canonical-event.js";
3
3
  import { detachCliBridgeReader } from "./retained-control.js";
4
4
  import { MAX_CLI_BRIDGE_CONTROL_RESPONSE_BYTES, MAX_CLI_BRIDGE_RESULT_RESPONSE_BYTES, readBoundedCliBridgeResponse, requestHeaders, trimSlash, } from "./transport.js";
5
- import { modelRequestsFromOpenAi, parseSse, responseIdentityFromOpenAi, safeJson, toolCallsFromDelta, usageFromOpenAi, } from "./wire.js";
5
+ import { modelRequestsFromOpenAi, parseSse, providerUsageFromOpenAi, responseIdentityFromOpenAi, safeJson, toolCallsFromDelta, usageFromOpenAi, } from "./wire.js";
6
6
  export class CliBridgeRequestRejectedError extends Error {
7
7
  status;
8
8
  constructor(status, detail) {
@@ -20,47 +20,8 @@ class CliBridgeRunCancelledError extends Error {
20
20
  }
21
21
  }
22
22
  export async function* streamCliBridgeTurn(options, turn, requestBody, transport, runId, lastEventId, signal, onResponse, readAuthoritativeRun, validateCanonicalEvent) {
23
- const reconnect = requestBody === undefined;
24
23
  const replay = retainedReplayRequest(lastEventId);
25
- const response = await transport.fetch(reconnect
26
- ? `${trimSlash(options.baseUrl)}/v1/runs/${encodeURIComponent(runId)}/events`
27
- : `${trimSlash(options.baseUrl)}/v1/chat/completions`, {
28
- method: reconnect ? "GET" : "POST",
29
- headers: {
30
- ...requestHeaders(options),
31
- accept: "text/event-stream",
32
- ...(!reconnect && turn.sessionId ? { "x-session-id": turn.sessionId } : {}),
33
- ...(replay.serverCursor === undefined
34
- ? {}
35
- : { "last-event-id": replay.serverCursor }),
36
- },
37
- ...(requestBody === undefined ? {} : { body: requestBody }),
38
- ...(signal ? { signal } : {}),
39
- });
40
- if (!response.ok) {
41
- let detail = "request rejected";
42
- try {
43
- detail = await readBoundedCliBridgeResponse(response, MAX_CLI_BRIDGE_CONTROL_RESPONSE_BYTES, signal);
44
- }
45
- catch {
46
- // The HTTP status already proves this request was rejected.
47
- }
48
- throw new CliBridgeRequestRejectedError(response.status, detail);
49
- }
50
- try {
51
- onResponse?.(response);
52
- }
53
- catch (error) {
54
- if (response.body) {
55
- try {
56
- await detachCliBridgeReader(response.body);
57
- }
58
- catch (detachError) {
59
- throw new AggregateError([error, detachError], `cli-bridge rejected response identity for run "${runId}" and could not detach its reader`);
60
- }
61
- }
62
- throw error;
63
- }
24
+ const response = await openCliBridgeStream([options, turn, requestBody, transport, runId, lastEventId, signal, onResponse]);
64
25
  if (!response.body)
65
26
  throw new Error("cli-bridge response body is empty");
66
27
  let text = "";
@@ -68,7 +29,6 @@ export async function* streamCliBridgeTurn(options, turn, requestBody, transport
68
29
  const messageId = turn.turnId ?? `${sessionId}:assistant`;
69
30
  const emittedToolCalls = new Set();
70
31
  let completed = false;
71
- let sawUsage = false;
72
32
  let observedModelRequests = 0;
73
33
  let modelRequestsKnown = false;
74
34
  let servedModel;
@@ -156,6 +116,8 @@ export async function* streamCliBridgeTurn(options, turn, requestBody, transport
156
116
  systemFingerprint = responseIdentity.system_fingerprint;
157
117
  }
158
118
  const identityData = {
119
+ ...(responseIdentity?.provider_models ? { provider_models: responseIdentity.provider_models } : {}),
120
+ ...(responseIdentity?.provider_model ? { provider_model: responseIdentity.provider_model } : {}),
159
121
  ...(servedModel === undefined ? {} : { model: servedModel }),
160
122
  ...(systemFingerprint === undefined ? {} : { system_fingerprint: systemFingerprint }),
161
123
  };
@@ -171,12 +133,11 @@ export async function* streamCliBridgeTurn(options, turn, requestBody, transport
171
133
  : rawChunk;
172
134
  if (rawChunk && toolCallDeltas.length === 0)
173
135
  textBoundaryPending = false;
174
- const nextUsage = usageFromOpenAi(parsed.usage);
136
+ const rawUsage = providerUsageFromOpenAi(parsed.usage);
137
+ const nextUsage = usageFromOpenAi(rawUsage);
175
138
  const nextModelRequests = modelRequestsFromOpenAi(parsed.usage);
176
139
  const frameEvents = [];
177
- if (nextUsage || nextModelRequests !== undefined) {
178
- if (nextUsage)
179
- sawUsage = true;
140
+ if (rawUsage || nextModelRequests !== undefined) {
180
141
  if (nextModelRequests !== undefined) {
181
142
  observedModelRequests += nextModelRequests;
182
143
  modelRequestsKnown = true;
@@ -188,6 +149,8 @@ export async function* streamCliBridgeTurn(options, turn, requestBody, transport
188
149
  ...(nextModelRequests === undefined ? {} : { modelRequests: nextModelRequests }),
189
150
  },
190
151
  ...(nextUsage ? { usage: nextUsage } : {}),
152
+ usageMode: "delta",
153
+ providerEvent: { usage: rawUsage },
191
154
  });
192
155
  }
193
156
  if (chunk) {
@@ -236,6 +199,9 @@ export async function* streamCliBridgeTurn(options, turn, requestBody, transport
236
199
  }
237
200
  if (toolCallDeltas.length > 0)
238
201
  textBoundaryPending = true;
202
+ if (responseIdentity?.provider_models && !frameEvents.some(event => event.data.provider_models !== undefined)) {
203
+ frameEvents.push({ type: "raw", data: identityData, providerEvent: parsed });
204
+ }
239
205
  if (choice?.finish_reason) {
240
206
  terminalFrameSeen = true;
241
207
  if (choice.finish_reason === "error") {
@@ -261,6 +227,58 @@ export async function* streamCliBridgeTurn(options, turn, requestBody, transport
261
227
  }
262
228
  yield* retainedEventsWithIdentity(frameEvents, frame.id, runId, sessionId, turn.executionId, replay.anchor);
263
229
  }
230
+ yield* finishCliBridgeStream([options, turn, requestBody, transport, runId, lastEventId, signal, onResponse, readAuthoritativeRun], { protocol, canonicalTerminalStatus, canonicalTerminalEvent, sawProtocolEnd, completed,
231
+ cancelledTerminalSeen, terminalFrame, text, modelRequestsKnown, observedModelRequests, servedModel, systemFingerprint }, replay.anchor);
232
+ }
233
+ /** Both dispatch and live readers use the same admission and bounded HTTP refusal. */
234
+ export async function openCliBridgeStream([options, turn, requestBody, transport, runId, lastEventId, signal, onResponse]) {
235
+ const reconnect = requestBody === undefined;
236
+ const replay = retainedReplayRequest(lastEventId);
237
+ const response = await transport.fetch(reconnect
238
+ ? `${trimSlash(options.baseUrl)}/v1/runs/${encodeURIComponent(runId)}/events`
239
+ : `${trimSlash(options.baseUrl)}/v1/chat/completions`, {
240
+ method: reconnect ? "GET" : "POST",
241
+ headers: {
242
+ ...requestHeaders(options),
243
+ accept: "text/event-stream",
244
+ ...(!reconnect && turn.sessionId ? { "x-session-id": turn.sessionId } : {}),
245
+ ...(replay.serverCursor === undefined
246
+ ? {}
247
+ : { "last-event-id": replay.serverCursor }),
248
+ },
249
+ ...(requestBody === undefined ? {} : { body: requestBody }),
250
+ ...(signal ? { signal } : {}),
251
+ });
252
+ if (!response.ok) {
253
+ let detail = "request rejected";
254
+ try {
255
+ detail = await readBoundedCliBridgeResponse(response, MAX_CLI_BRIDGE_CONTROL_RESPONSE_BYTES, signal);
256
+ }
257
+ catch {
258
+ // The HTTP status already proves this request was rejected.
259
+ }
260
+ throw new CliBridgeRequestRejectedError(response.status, detail);
261
+ }
262
+ try {
263
+ onResponse?.(response);
264
+ }
265
+ catch (error) {
266
+ if (response.body) {
267
+ try {
268
+ await detachCliBridgeReader(response.body);
269
+ }
270
+ catch (detachError) {
271
+ throw new AggregateError([error, detachError], `cli-bridge rejected response identity for run "${runId}" and could not detach its reader`);
272
+ }
273
+ }
274
+ throw error;
275
+ }
276
+ return response;
277
+ }
278
+ /** Confirm terminal authority before releasing the final frame and its cumulative receipt. */
279
+ async function* finishCliBridgeStream([options, turn, requestBody, transport, runId, lastEventId, signal, onResponse, readAuthoritativeRun], { protocol, canonicalTerminalStatus, canonicalTerminalEvent, sawProtocolEnd, completed, cancelledTerminalSeen, terminalFrame, text, modelRequestsKnown, observedModelRequests, servedModel, systemFingerprint }, anchor) {
280
+ const sessionId = turn.sessionId ?? runId;
281
+ const reconnect = requestBody === undefined;
264
282
  if (protocol === "canonical") {
265
283
  if (canonicalTerminalStatus === undefined) {
266
284
  throw new Error("cli-bridge canonical stream ended without terminal status");
@@ -359,7 +377,7 @@ export async function* streamCliBridgeTurn(options, turn, requestBody, transport
359
377
  finalText: lastEventId === undefined ? text : result.text,
360
378
  finishReason: lastEventId === undefined ? terminalFrame.finishReason : result.finishReason,
361
379
  status: "completed",
362
- ...(result.model === undefined ? {} : { model: result.model }),
380
+ ...result.identity,
363
381
  ...(result.system_fingerprint === undefined
364
382
  ? {}
365
383
  : { system_fingerprint: result.system_fingerprint }),
@@ -367,7 +385,9 @@ export async function* streamCliBridgeTurn(options, turn, requestBody, transport
367
385
  ? {}
368
386
  : { modelRequests: finalModelRequests }),
369
387
  },
370
- ...(result.usage && !sawUsage ? { usage: result.usage } : {}),
388
+ ...(result.usage ? { usage: result.usage } : {}),
389
+ ...(result.providerUsage ? { usageMode: "cumulative" } : {}),
390
+ ...(result.providerUsage ? { providerEvent: { usage: result.providerUsage } } : {}),
371
391
  };
372
392
  }
373
393
  }
@@ -401,7 +421,7 @@ export async function* streamCliBridgeTurn(options, turn, requestBody, transport
401
421
  throw new Error("cli-bridge terminal state did not produce a terminal event");
402
422
  }
403
423
  terminalFrame.events.push(terminalEvent);
404
- yield* retainedEventsWithIdentity(terminalFrame.events, terminalFrame.id, runId, sessionId, turn.executionId, replay.anchor);
424
+ yield* retainedEventsWithIdentity(terminalFrame.events, terminalFrame.id, runId, sessionId, turn.executionId, anchor);
405
425
  if (terminalFailure)
406
426
  throw terminalFailure;
407
427
  }
@@ -476,6 +496,8 @@ async function readFullCliBridgeResult(options, requestBody, transport, signal,
476
496
  text: message.content,
477
497
  finishReason: choice.finish_reason,
478
498
  ...(usage ? { usage } : {}),
499
+ providerUsage: providerUsageFromOpenAi(parsed?.usage),
500
+ identity: responseIdentity,
479
501
  ...(modelRequests === undefined ? {} : { modelRequests }),
480
502
  ...(responseIdentity?.model === undefined ? {} : { model: responseIdentity.model }),
481
503
  ...(responseIdentity?.system_fingerprint === undefined
@@ -0,0 +1,60 @@
1
+ import type { HarnessType } from "@tangle-network/agent-interface";
2
+ /**
3
+ * The box: one image every cloud runs, holding Node, CLI Bridge and the pinned
4
+ * harness CLIs. The Bridge listens on {@link BRIDGE_PORT}, materializes the
5
+ * exact profile and starts the selected harness exactly as it does for Braid
6
+ * on a local machine.
7
+ */
8
+ export declare const BRIDGE_PORT = 3344;
9
+ /** The box user's home. */
10
+ export declare const BOX_HOME = "/home/node";
11
+ /** The default workspace directory; `workspace.cwd` with `base: "repository"` resolves under it. */
12
+ export declare const BOX_WORKSPACE_DIR = "/home/node/workspace";
13
+ export declare const BRIDGE_DATA_DIR = "/home/node/.cli-bridge";
14
+ export declare const BOX_NODE_IMAGE = "node:24.22.1-trixie-slim";
15
+ /**
16
+ * The CLI Bridge source revision the box installs, reviewed as a commit.
17
+ * `agentProfileMaterialize` is the exact version of the planner that revision
18
+ * pins; {@link createBridgeSandboxProvider} plans with the same version, and
19
+ * the image build refuses a revision whose pin differs.
20
+ */
21
+ export declare const BOX_PINS: {
22
+ readonly cliBridgeRepository: "drewstone/cli-bridge";
23
+ readonly cliBridgeCommit: "263f700dc5ca8b7b6e53b986d376102e0d82b42f";
24
+ readonly agentProfileMaterialize: "1.1.4";
25
+ };
26
+ export interface BoxRouter {
27
+ /** Router root URL, without a path. */
28
+ baseUrl: string;
29
+ /** The per-run capped Router key. */
30
+ key: string;
31
+ }
32
+ /**
33
+ * A harness the box carries and routes through the Tangle Router.
34
+ *
35
+ * A harness joins this table only after a live turn through the Router
36
+ * completed in a box, so the declared set is the proven set.
37
+ */
38
+ export interface BoxHarness {
39
+ readonly harness: HarnessType;
40
+ /** The Bridge backend name in `BRIDGE_BACKENDS`. */
41
+ readonly bridgeBackend: string;
42
+ /** The npm package that installs the CLI. */
43
+ readonly npmPackage: string;
44
+ /** The CLI binary; `<bin> --version` prints the pinned version first. */
45
+ readonly bin: string;
46
+ /** Environment that points this harness at the Router, set on the Bridge process only. */
47
+ routerEnv(router: BoxRouter): Record<string, string>;
48
+ }
49
+ export declare const BOX_HARNESSES: Readonly<Partial<Record<HarnessType, BoxHarness>>>;
50
+ export declare function boxHarness(harness: string | undefined): BoxHarness | undefined;
51
+ /** The CLI version the harness registry pins; the box image installs exactly this. */
52
+ export declare function boxHarnessVersion(harness: HarnessType): string;
53
+ /** The command that starts the Bridge; its settings come from the process environment. */
54
+ export declare const BRIDGE_START_COMMAND = "cli-bridge";
55
+ /**
56
+ * The Bridge process environment. `bearer` guards the exposed port; the Router
57
+ * variables reach the harness through the Bridge's child environment and
58
+ * appear in no command line or file.
59
+ */
60
+ export declare function bridgeProcessEnv(harness: BoxHarness, bearer: string, router: BoxRouter): Record<string, string>;
@@ -0,0 +1,68 @@
1
+ import { NATIVE_SESSION_CONTRACTS } from "@tangle-network/cli-agent-registry/native-session-contracts";
2
+ /**
3
+ * The box: one image every cloud runs, holding Node, CLI Bridge and the pinned
4
+ * harness CLIs. The Bridge listens on {@link BRIDGE_PORT}, materializes the
5
+ * exact profile and starts the selected harness exactly as it does for Braid
6
+ * on a local machine.
7
+ */
8
+ export const BRIDGE_PORT = 3344;
9
+ /** The box user's home. */
10
+ export const BOX_HOME = "/home/node";
11
+ /** The default workspace directory; `workspace.cwd` with `base: "repository"` resolves under it. */
12
+ export const BOX_WORKSPACE_DIR = `${BOX_HOME}/workspace`;
13
+ export const BRIDGE_DATA_DIR = `${BOX_HOME}/.cli-bridge`;
14
+ export const BOX_NODE_IMAGE = "node:24.22.1-trixie-slim";
15
+ /**
16
+ * The CLI Bridge source revision the box installs, reviewed as a commit.
17
+ * `agentProfileMaterialize` is the exact version of the planner that revision
18
+ * pins; {@link createBridgeSandboxProvider} plans with the same version, and
19
+ * the image build refuses a revision whose pin differs.
20
+ */
21
+ export const BOX_PINS = {
22
+ cliBridgeRepository: "drewstone/cli-bridge",
23
+ cliBridgeCommit: "263f700dc5ca8b7b6e53b986d376102e0d82b42f",
24
+ agentProfileMaterialize: "1.1.4",
25
+ };
26
+ export const BOX_HARNESSES = {
27
+ "claude-code": {
28
+ harness: "claude-code",
29
+ bridgeBackend: "claude",
30
+ npmPackage: "@anthropic-ai/claude-code",
31
+ bin: "claude",
32
+ // The Router authenticates a Bearer token. ANTHROPIC_API_KEY sends x-api-key, which it rejects.
33
+ routerEnv: (router) => ({
34
+ ANTHROPIC_BASE_URL: router.baseUrl,
35
+ ANTHROPIC_AUTH_TOKEN: router.key,
36
+ }),
37
+ },
38
+ };
39
+ export function boxHarness(harness) {
40
+ return harness === undefined ? undefined : BOX_HARNESSES[harness];
41
+ }
42
+ /** The CLI version the harness registry pins; the box image installs exactly this. */
43
+ export function boxHarnessVersion(harness) {
44
+ const contract = NATIVE_SESSION_CONTRACTS[harness];
45
+ if (contract?.cliVersion === undefined) {
46
+ throw new Error(`cli-agent-registry pins no CLI version for harness ${harness}`);
47
+ }
48
+ return contract.cliVersion;
49
+ }
50
+ /** The command that starts the Bridge; its settings come from the process environment. */
51
+ export const BRIDGE_START_COMMAND = "cli-bridge";
52
+ /**
53
+ * The Bridge process environment. `bearer` guards the exposed port; the Router
54
+ * variables reach the harness through the Bridge's child environment and
55
+ * appear in no command line or file.
56
+ */
57
+ export function bridgeProcessEnv(harness, bearer, router) {
58
+ return {
59
+ BRIDGE_HOST: "0.0.0.0",
60
+ BRIDGE_PORT: String(BRIDGE_PORT),
61
+ BRIDGE_BEARER: bearer,
62
+ BRIDGE_BACKENDS: harness.bridgeBackend,
63
+ BRIDGE_DATA_DIR: BRIDGE_DATA_DIR,
64
+ BRIDGE_JAIL_MODE: "off",
65
+ HOME: BOX_HOME,
66
+ ...harness.routerEnv(router),
67
+ };
68
+ }