sentinelayer-cli 0.31.4 → 0.31.5
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/package.json +1 -1
- package/src/ai/client.js +71 -0
- package/src/session/daemon.js +23 -0
- package/src/session/usage.js +3 -1
package/package.json
CHANGED
package/src/ai/client.js
CHANGED
|
@@ -146,6 +146,70 @@ function extractTextFromResponse(provider, payload) {
|
|
|
146
146
|
return "";
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
function normalizeUsageNumber(value) {
|
|
150
|
+
const parsed = Number(value);
|
|
151
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
152
|
+
return 0;
|
|
153
|
+
}
|
|
154
|
+
return Math.floor(parsed);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function buildUsagePayload({ provider, model, inputTokens = 0, outputTokens = 0, totalTokens = 0 } = {}) {
|
|
158
|
+
const input = normalizeUsageNumber(inputTokens);
|
|
159
|
+
const output = normalizeUsageNumber(outputTokens);
|
|
160
|
+
const total = normalizeUsageNumber(totalTokens) || input + output;
|
|
161
|
+
if (total <= 0 && input + output <= 0) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
inputTokens: input,
|
|
166
|
+
outputTokens: output,
|
|
167
|
+
totalTokens: total,
|
|
168
|
+
model,
|
|
169
|
+
provider,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function extractUsageFromResponse(provider, payload, model) {
|
|
174
|
+
if (!payload || typeof payload !== "object") {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (provider === "openai") {
|
|
179
|
+
const usage = payload.usage || {};
|
|
180
|
+
return buildUsagePayload({
|
|
181
|
+
provider,
|
|
182
|
+
model,
|
|
183
|
+
inputTokens: usage.prompt_tokens ?? usage.input_tokens,
|
|
184
|
+
outputTokens: usage.completion_tokens ?? usage.output_tokens,
|
|
185
|
+
totalTokens: usage.total_tokens,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (provider === "anthropic") {
|
|
190
|
+
const usage = payload.usage || {};
|
|
191
|
+
return buildUsagePayload({
|
|
192
|
+
provider,
|
|
193
|
+
model,
|
|
194
|
+
inputTokens: usage.input_tokens,
|
|
195
|
+
outputTokens: usage.output_tokens,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (provider === "google") {
|
|
200
|
+
const usage = payload.usageMetadata || payload.usage_metadata || {};
|
|
201
|
+
return buildUsagePayload({
|
|
202
|
+
provider,
|
|
203
|
+
model,
|
|
204
|
+
inputTokens: usage.promptTokenCount ?? usage.prompt_token_count,
|
|
205
|
+
outputTokens: usage.candidatesTokenCount ?? usage.candidates_token_count,
|
|
206
|
+
totalTokens: usage.totalTokenCount ?? usage.total_token_count,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
|
|
149
213
|
function buildProviderRequest({ provider, apiKey, model, prompt, stream }) {
|
|
150
214
|
const normalizedProvider = normalizeProvider(provider);
|
|
151
215
|
const normalizedPrompt = normalizePrompt(prompt);
|
|
@@ -527,6 +591,13 @@ export class MultiProviderApiClient {
|
|
|
527
591
|
} else {
|
|
528
592
|
const payload = await response.json();
|
|
529
593
|
text = extractTextFromResponse(resolvedProvider, payload);
|
|
594
|
+
const usage = extractUsageFromResponse(resolvedProvider, payload, resolvedModel);
|
|
595
|
+
return {
|
|
596
|
+
provider: resolvedProvider,
|
|
597
|
+
model: resolvedModel,
|
|
598
|
+
text,
|
|
599
|
+
usage: usage || undefined,
|
|
600
|
+
};
|
|
530
601
|
}
|
|
531
602
|
|
|
532
603
|
return {
|
package/src/session/daemon.js
CHANGED
|
@@ -44,6 +44,7 @@ import { stopRuntimeRunsForSession } from "./runtime-bridge.js";
|
|
|
44
44
|
import { getSession, renewSession } from "./store.js";
|
|
45
45
|
import { appendToStream, readStream, tailStream } from "./stream.js";
|
|
46
46
|
import { handleTaskDirective } from "./tasks.js";
|
|
47
|
+
import { emitLLMInteraction } from "./usage.js";
|
|
47
48
|
|
|
48
49
|
const DAEMON_TICK_INTERVAL_MS = 30_000;
|
|
49
50
|
const HELP_REQUEST_TIMEOUT_MS = 1_200;
|
|
@@ -653,6 +654,28 @@ async function maybeRespondToHelpRequest(
|
|
|
653
654
|
nowIso,
|
|
654
655
|
}
|
|
655
656
|
);
|
|
657
|
+
if (normalizeUsageNumber(response.usage.inputTokens) + normalizeUsageNumber(response.usage.outputTokens) > 0) {
|
|
658
|
+
// The managed proxy already records the durable API billing/v1 row for
|
|
659
|
+
// this same idempotency key. Keep this copy local for immediate transcript
|
|
660
|
+
// and recap usage without double-counting Session.total_cost_usd remotely.
|
|
661
|
+
await emitLLMInteraction(daemonState.sessionId, {
|
|
662
|
+
agentId: SENTI_IDENTITY.id,
|
|
663
|
+
agentModel: response.usage.model || daemonState.model,
|
|
664
|
+
role: SENTI_IDENTITY.role,
|
|
665
|
+
inputTokens: response.usage.inputTokens,
|
|
666
|
+
outputTokens: response.usage.outputTokens,
|
|
667
|
+
costUsd: response.usage.costUsd,
|
|
668
|
+
durationMs: response.usage.latencyMs,
|
|
669
|
+
action: "proxy_llm",
|
|
670
|
+
provider: response.usage.provider || "sentinelayer",
|
|
671
|
+
billingTier: "internal",
|
|
672
|
+
prompt: normalizeString(requestEvent?.payload?.message) || normalizeString(requestEvent?.payload?.request),
|
|
673
|
+
response: response.message,
|
|
674
|
+
interactionId: `senti:${daemonState.sessionId}:help:${requestId}`,
|
|
675
|
+
targetPath,
|
|
676
|
+
syncRemote: false,
|
|
677
|
+
});
|
|
678
|
+
}
|
|
656
679
|
return responseEvent;
|
|
657
680
|
}
|
|
658
681
|
|
package/src/session/usage.js
CHANGED
|
@@ -111,6 +111,7 @@ function computedCost({ model, inputTokens, outputTokens }) {
|
|
|
111
111
|
* @param {string} [params.response] full response text (clipped)
|
|
112
112
|
* @param {string} [params.interactionId] opaque id for cross-event correlation
|
|
113
113
|
* @param {string} [params.targetPath] workspace path (default cwd)
|
|
114
|
+
* @param {boolean} [params.syncRemote] sync local usage event to API (default true)
|
|
114
115
|
* @returns {Promise<{ event: string, interactionId: string, totalTokens: number, costUsd: number }>}
|
|
115
116
|
*/
|
|
116
117
|
export async function emitLLMInteraction(
|
|
@@ -132,6 +133,7 @@ export async function emitLLMInteraction(
|
|
|
132
133
|
response = "",
|
|
133
134
|
interactionId = "",
|
|
134
135
|
targetPath = process.cwd(),
|
|
136
|
+
syncRemote = true,
|
|
135
137
|
} = {},
|
|
136
138
|
) {
|
|
137
139
|
const sid = n(sessionId);
|
|
@@ -219,7 +221,7 @@ export async function emitLLMInteraction(
|
|
|
219
221
|
ts,
|
|
220
222
|
});
|
|
221
223
|
|
|
222
|
-
await appendToStream(paths.sessionId, envelope, { targetPath });
|
|
224
|
+
await appendToStream(paths.sessionId, envelope, { targetPath, syncRemote });
|
|
223
225
|
return {
|
|
224
226
|
event: SESSION_USAGE_EVENT,
|
|
225
227
|
interactionId: id,
|