@tangle-network/agent-provider-cli-bridge 2.0.1 → 2.0.3
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 +15 -0
- package/dist/retained-canonical-event.d.ts +1 -1
- package/dist/retained-canonical-event.js +7 -33
- package/dist/retained-environment.js +2 -3
- package/dist/retained-execution.d.ts +2 -2
- package/dist/retained-execution.js +73 -55
- package/dist/retained-stream.d.ts +2 -0
- package/dist/retained-stream.js +71 -49
- package/dist/wire.d.ts +4 -1
- package/dist/wire.js +43 -29
- package/package.json +8 -8
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.
|
|
@@ -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
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
//
|
|
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
|
-
/**
|
|
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 {
|
|
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
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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
|
-
|
|
409
|
-
|
|
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>;
|
package/dist/retained-stream.js
CHANGED
|
@@ -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
|
|
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
|
|
136
|
+
const rawUsage = providerUsageFromOpenAi(parsed.usage);
|
|
137
|
+
const nextUsage = usageFromOpenAi(rawUsage);
|
|
175
138
|
const nextModelRequests = modelRequestsFromOpenAi(parsed.usage);
|
|
176
139
|
const frameEvents = [];
|
|
177
|
-
if (
|
|
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
|
-
...
|
|
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
|
|
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,
|
|
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
|
package/dist/wire.d.ts
CHANGED
|
@@ -27,11 +27,14 @@ export declare function toolCallsFromDelta(value: unknown): Array<{
|
|
|
27
27
|
}>;
|
|
28
28
|
export declare function safeJson(value: string): Record<string, unknown> | null;
|
|
29
29
|
export declare function usageFromOpenAi(value: unknown): TokenUsage | undefined;
|
|
30
|
+
export declare function providerUsageFromOpenAi(value: unknown): Record<string, unknown> | undefined;
|
|
30
31
|
export declare function modelRequestsFromOpenAi(value: unknown): number | undefined;
|
|
31
|
-
/**
|
|
32
|
+
/** Compatibility model and explicit provider observations have different authority. */
|
|
32
33
|
export declare function responseIdentityFromOpenAi(value: unknown): {
|
|
33
34
|
model?: string;
|
|
34
35
|
system_fingerprint?: string;
|
|
36
|
+
provider_models?: string[];
|
|
37
|
+
provider_model?: string;
|
|
35
38
|
} | undefined;
|
|
36
39
|
export declare function modelRequestCount(value: unknown): number | undefined;
|
|
37
40
|
export declare function cliBridgeRunId(environmentId: string, turn: AgentTurnInput, turnId: string): string;
|
package/dist/wire.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { AgentExactRunControlRefSchema, RequestedInteractionsSchema, WorkspaceRequestSchema, workspaceCwdPathForBase, } from "@tangle-network/agent-interface";
|
|
2
|
+
import { AgentExactRunControlRefSchema, CONTRACT_MAX_ARRAY_LENGTH, RequestedInteractionsSchema, WorkspaceRequestSchema, workspaceCwdPathForBase, } from "@tangle-network/agent-interface";
|
|
3
3
|
export function toChatCompletionsBody(options, environmentInput, turn, coordinates) {
|
|
4
4
|
const profile = inlineProfile(environmentInput.profile);
|
|
5
5
|
const execution = executionFromInput(options, environmentInput);
|
|
@@ -257,47 +257,61 @@ export function safeJson(value) {
|
|
|
257
257
|
}
|
|
258
258
|
}
|
|
259
259
|
export function usageFromOpenAi(value) {
|
|
260
|
-
|
|
260
|
+
const record = providerUsageFromOpenAi(value);
|
|
261
|
+
if (!record)
|
|
261
262
|
return undefined;
|
|
262
|
-
const
|
|
263
|
-
const
|
|
264
|
-
nonNegativeSafeInteger(record.input_tokens);
|
|
265
|
-
const outputTokens = nonNegativeSafeInteger(record.completion_tokens) ??
|
|
266
|
-
nonNegativeSafeInteger(record.output_tokens);
|
|
263
|
+
const inputTokens = nonNegativeSafeInteger(record.prompt_tokens) ?? nonNegativeSafeInteger(record.input_tokens);
|
|
264
|
+
const outputTokens = nonNegativeSafeInteger(record.completion_tokens) ?? nonNegativeSafeInteger(record.output_tokens);
|
|
267
265
|
if (inputTokens === undefined || outputTokens === undefined)
|
|
268
266
|
return undefined;
|
|
269
|
-
const
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
267
|
+
const usage = { inputTokens, outputTokens };
|
|
268
|
+
for (const [key, wireKey] of [
|
|
269
|
+
["totalTokens", "total_tokens"], ["cacheReadInputTokens", "cache_read_input_tokens"],
|
|
270
|
+
["cacheCreationInputTokens", "cache_write_input_tokens"], ["reasoningTokens", "reasoning_tokens"],
|
|
271
|
+
]) {
|
|
272
|
+
const count = nonNegativeSafeInteger(record[wireKey]) ??
|
|
273
|
+
(key === "cacheCreationInputTokens" ? nonNegativeSafeInteger(record.cache_creation_input_tokens) : undefined);
|
|
274
|
+
if (count !== undefined)
|
|
275
|
+
usage[key] = count;
|
|
276
|
+
}
|
|
273
277
|
const cost = finiteNonNegativeNumber(record.cost);
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
...(cost !== undefined ? { cost } : {}),
|
|
282
|
-
};
|
|
278
|
+
if (cost !== undefined)
|
|
279
|
+
usage.cost = cost;
|
|
280
|
+
return usage;
|
|
281
|
+
}
|
|
282
|
+
export function providerUsageFromOpenAi(value) {
|
|
283
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
284
|
+
? value : undefined;
|
|
283
285
|
}
|
|
284
286
|
export function modelRequestsFromOpenAi(value) {
|
|
285
287
|
if (!value || typeof value !== "object")
|
|
286
288
|
return undefined;
|
|
287
289
|
return modelRequestCount(value.model_requests);
|
|
288
290
|
}
|
|
289
|
-
/**
|
|
291
|
+
/** Compatibility model and explicit provider observations have different authority. */
|
|
290
292
|
export function responseIdentityFromOpenAi(value) {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
const record = value;
|
|
294
|
-
const model = nonEmptyString(record.model);
|
|
295
|
-
const systemFingerprint = nonEmptyString(record.system_fingerprint);
|
|
296
|
-
if (model === undefined && systemFingerprint === undefined)
|
|
293
|
+
const record = providerUsageFromOpenAi(value);
|
|
294
|
+
if (!record)
|
|
297
295
|
return undefined;
|
|
296
|
+
if (record.provider_model !== undefined && nonEmptyString(record.provider_model) === undefined) {
|
|
297
|
+
throw new Error("cli-bridge returned an invalid provider model observation");
|
|
298
|
+
}
|
|
299
|
+
const models = record.provider_models;
|
|
300
|
+
if (models !== undefined && (!Array.isArray(models) || models.length > CONTRACT_MAX_ARRAY_LENGTH ||
|
|
301
|
+
models.some(model => nonEmptyString(model) === undefined))) {
|
|
302
|
+
throw new Error("cli-bridge returned invalid provider model observations");
|
|
303
|
+
}
|
|
304
|
+
const observations = [...new Set([
|
|
305
|
+
...(models ?? []),
|
|
306
|
+
...(nonEmptyString(record.provider_model) ? [record.provider_model] : []),
|
|
307
|
+
])];
|
|
308
|
+
if (observations.length > CONTRACT_MAX_ARRAY_LENGTH)
|
|
309
|
+
throw new Error("cli-bridge provider model observations exceed contract bounds");
|
|
298
310
|
return {
|
|
299
|
-
...(model
|
|
300
|
-
...(
|
|
311
|
+
...(nonEmptyString(record.model) ? { model: record.model } : {}),
|
|
312
|
+
...(nonEmptyString(record.system_fingerprint) ? { system_fingerprint: record.system_fingerprint } : {}),
|
|
313
|
+
...(observations.length ? { provider_models: observations } : {}),
|
|
314
|
+
...(observations.length === 1 ? { provider_model: observations[0] } : {}),
|
|
301
315
|
};
|
|
302
316
|
}
|
|
303
317
|
export function modelRequestCount(value) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tangle-network/agent-provider-cli-bridge",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.3",
|
|
4
4
|
"description": "AgentEnvironmentProvider adapter for cli-bridge retained sessions and one-shot turns",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -64,20 +64,20 @@
|
|
|
64
64
|
"LICENSE"
|
|
65
65
|
],
|
|
66
66
|
"dependencies": {
|
|
67
|
-
"@tangle-network/agent-interface": "^5.
|
|
68
|
-
"undici": "8.
|
|
67
|
+
"@tangle-network/agent-interface": "^5.10.2",
|
|
68
|
+
"undici": "8.12.3"
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
|
-
"@tangle-network/agent-provider-testkit": "0.12.
|
|
72
|
-
"@types/node": "26.6.
|
|
73
|
-
"typescript": "
|
|
74
|
-
"vitest": "5.0.
|
|
71
|
+
"@tangle-network/agent-provider-testkit": "0.12.1",
|
|
72
|
+
"@types/node": "26.6.5",
|
|
73
|
+
"typescript": "6.0.3",
|
|
74
|
+
"vitest": "5.0.5"
|
|
75
75
|
},
|
|
76
76
|
"scripts": {
|
|
77
77
|
"build": "pnpm run clean && tsc -p tsconfig.build.json",
|
|
78
78
|
"check-types": "tsc --noEmit",
|
|
79
79
|
"clean": "rm -rf dist",
|
|
80
80
|
"test": "vitest run src",
|
|
81
|
-
"test:integration": "test -n \"$CLI_BRIDGE_INTEGRATION_ROOT\" || { echo 'CLI_BRIDGE_INTEGRATION_ROOT is required; see README' >&2; exit 1; }; pnpm run build && vitest run tests
|
|
81
|
+
"test:integration": "test -n \"$CLI_BRIDGE_INTEGRATION_ROOT\" || { echo 'CLI_BRIDGE_INTEGRATION_ROOT is required; see README' >&2; exit 1; }; pnpm run build && vitest run tests"
|
|
82
82
|
}
|
|
83
83
|
}
|