@omnicross/core 0.1.9 → 0.1.10

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.
Files changed (39) hide show
  1. package/dist/{chunk-EPN67EGP.cjs → chunk-5UK3KLE2.cjs} +9 -1
  2. package/dist/chunk-6D4W22P4.js +150 -0
  3. package/dist/{chunk-UAPBLNN2.js → chunk-AQDON6MB.js} +8 -0
  4. package/dist/{chunk-CQIJCPMF.js → chunk-FEBAQI5A.js} +78 -15
  5. package/dist/{chunk-7C7DET6S.js → chunk-GXEZ2R3E.js} +156 -1
  6. package/dist/chunk-QUSEZNYS.cjs +150 -0
  7. package/dist/{chunk-XELFM7KL.cjs → chunk-TIT74RIL.cjs} +235 -172
  8. package/dist/{chunk-XPIOJQS2.cjs → chunk-UWDW6PN3.cjs} +159 -4
  9. package/dist/completion/CompletionService.cjs +5 -5
  10. package/dist/completion/CompletionService.js +4 -4
  11. package/dist/completion.cjs +5 -5
  12. package/dist/completion.js +4 -4
  13. package/dist/index.cjs +5 -5
  14. package/dist/index.js +4 -4
  15. package/dist/outbound-api/auditCapture.cjs +2 -2
  16. package/dist/outbound-api/auditCapture.d.cts +6 -0
  17. package/dist/outbound-api/auditCapture.d.ts +6 -0
  18. package/dist/outbound-api/auditCapture.js +1 -1
  19. package/dist/outbound-api.cjs +5 -5
  20. package/dist/outbound-api.d.cts +2 -1
  21. package/dist/outbound-api.d.ts +2 -1
  22. package/dist/outbound-api.js +4 -4
  23. package/dist/provider-proxy/ProviderProxy.cjs +5 -5
  24. package/dist/provider-proxy/ProviderProxy.js +4 -4
  25. package/dist/provider-proxy/ingress/providerProxyShared.cjs +5 -5
  26. package/dist/provider-proxy/ingress/providerProxyShared.js +4 -4
  27. package/dist/provider-proxy.cjs +5 -5
  28. package/dist/provider-proxy.js +4 -4
  29. package/dist/usage/usage-recorder.cjs +2 -2
  30. package/dist/usage/usage-recorder.d.cts +12 -2
  31. package/dist/usage/usage-recorder.d.ts +12 -2
  32. package/dist/usage/usage-recorder.js +1 -1
  33. package/dist/usage.cjs +20 -3
  34. package/dist/usage.d.cts +149 -0
  35. package/dist/usage.d.ts +149 -0
  36. package/dist/usage.js +20 -3
  37. package/package.json +2 -2
  38. package/dist/chunk-7VU7V2E4.js +0 -0
  39. package/dist/chunk-EYZYXJTJ.cjs +0 -1
@@ -10,12 +10,14 @@ var UsageRecorder = class {
10
10
  this.logger = logger;
11
11
  this.defer = _nullishCoalesce(options.defer, () => ( ((fn) => setTimeout(fn, 0))));
12
12
  this.onRecord = options.onRecord;
13
+ this.onEvent = options.onEvent;
13
14
  }
14
15
 
15
16
 
16
17
 
17
18
 
18
19
 
20
+
19
21
  /**
20
22
  * Record one LLM request. Returns immediately — the actual store insert is
21
23
  * deferred so the caller's response path is unblocked.
@@ -85,6 +87,12 @@ var UsageRecorder = class {
85
87
  ...input.cacheKeySource !== void 0 ? { cacheKeySource: input.cacheKeySource } : {},
86
88
  ...input.cacheKeyInjected !== void 0 ? { cacheKeyInjected: input.cacheKeyInjected } : {}
87
89
  };
90
+ if (this.onEvent) {
91
+ try {
92
+ this.onEvent(row, Date.now());
93
+ } catch (e3) {
94
+ }
95
+ }
88
96
  try {
89
97
  return await this.store.insert(row);
90
98
  } catch (err) {
@@ -118,7 +126,7 @@ var UsageRecorder = class {
118
126
  var safeStringify = (value) => {
119
127
  try {
120
128
  return JSON.stringify(value);
121
- } catch (e3) {
129
+ } catch (e4) {
122
130
  return "";
123
131
  }
124
132
  };
@@ -0,0 +1,150 @@
1
+ // src/usage/throughputTracker.ts
2
+ var THROUGHPUT_RETENTION_MS = 15 * 6e4;
3
+ var THROUGHPUT_SAMPLE_LIMIT = 2e4;
4
+ var THROUGHPUT_WINDOWS_MS = [6e4, 3e5, 9e5];
5
+ var THROUGHPUT_BUCKET_COUNT = 30;
6
+ var COMPACT_THRESHOLD = 1024;
7
+ var UsageThroughputTracker = class {
8
+ samples = [];
9
+ /** Index of the oldest live sample; slots before it are consumed. */
10
+ head = 0;
11
+ startedAt;
12
+ /** Newest ts dropped by the sample CAP (not by retention). Null when none. */
13
+ evictedThroughTs = null;
14
+ constructor(now = Date.now()) {
15
+ this.startedAt = now;
16
+ }
17
+ /** Record one served request. O(1) amortised; never touches disk. */
18
+ record(input, now = Date.now()) {
19
+ this.samples.push({
20
+ ts: input.ts ?? now,
21
+ inputTokens: input.inputTokens,
22
+ outputTokens: input.outputTokens,
23
+ cacheReadTokens: input.cacheReadTokens,
24
+ cacheCreationTokens: input.cacheCreationTokens,
25
+ reasoningTokens: input.reasoningTokens,
26
+ costUsd: input.costUsd
27
+ });
28
+ this.prune(now);
29
+ }
30
+ /** Aggregate every reported window plus the shared trend series. */
31
+ snapshot(now = Date.now()) {
32
+ this.prune(now);
33
+ return {
34
+ available: true,
35
+ collectedAt: now,
36
+ startedAt: this.startedAt,
37
+ retentionMs: THROUGHPUT_RETENTION_MS,
38
+ bucketMs: THROUGHPUT_RETENTION_MS / THROUGHPUT_BUCKET_COUNT,
39
+ windows: THROUGHPUT_WINDOWS_MS.map((windowMs) => this.window(windowMs, now)),
40
+ buckets: this.buckets(now)
41
+ };
42
+ }
43
+ /** Drop all samples (tests / teardown). `startedAt` is unchanged. */
44
+ clear() {
45
+ this.samples = [];
46
+ this.head = 0;
47
+ this.evictedThroughTs = null;
48
+ }
49
+ /**
50
+ * Drop samples past the retention horizon, then enforce the sample cap.
51
+ *
52
+ * Retention pruning uses the CALLER's `now`, which is never later than a
53
+ * subsequent snapshot's `now`, so it can only ever keep MORE than the widest
54
+ * window needs — it can never shave data off the 15-minute window.
55
+ */
56
+ prune(now) {
57
+ const cutoff = now - THROUGHPUT_RETENTION_MS;
58
+ while (this.head < this.samples.length && this.samples[this.head].ts < cutoff) {
59
+ this.head += 1;
60
+ }
61
+ while (this.samples.length - this.head > THROUGHPUT_SAMPLE_LIMIT) {
62
+ const dropped = this.samples[this.head];
63
+ this.evictedThroughTs = this.evictedThroughTs === null ? dropped.ts : Math.max(this.evictedThroughTs, dropped.ts);
64
+ this.head += 1;
65
+ }
66
+ if (this.head >= COMPACT_THRESHOLD) {
67
+ this.samples = this.samples.slice(this.head);
68
+ this.head = 0;
69
+ }
70
+ }
71
+ window(windowMs, now) {
72
+ const startTs = now - windowMs;
73
+ const row = {
74
+ windowMs,
75
+ requests: 0,
76
+ inputTokens: 0,
77
+ outputTokens: 0,
78
+ cacheReadTokens: 0,
79
+ cacheCreationTokens: 0,
80
+ reasoningTokens: 0,
81
+ totalTokens: 0,
82
+ costUsd: 0,
83
+ requestsPerMinute: 0,
84
+ tokensPerMinute: 0,
85
+ inputTokensPerMinute: 0,
86
+ outputTokensPerMinute: 0,
87
+ costUsdPerMinute: 0,
88
+ complete: this.evictedThroughTs === null || this.evictedThroughTs < startTs
89
+ };
90
+ for (let i = this.head; i < this.samples.length; i += 1) {
91
+ const sample = this.samples[i];
92
+ if (sample.ts < startTs) continue;
93
+ row.requests += 1;
94
+ row.inputTokens += sample.inputTokens;
95
+ row.outputTokens += sample.outputTokens;
96
+ row.cacheReadTokens += sample.cacheReadTokens;
97
+ row.cacheCreationTokens += sample.cacheCreationTokens;
98
+ row.reasoningTokens += sample.reasoningTokens;
99
+ row.costUsd += sample.costUsd;
100
+ }
101
+ row.totalTokens = row.inputTokens + row.outputTokens + row.cacheReadTokens + row.cacheCreationTokens;
102
+ const minutes = windowMs / 6e4;
103
+ row.requestsPerMinute = row.requests / minutes;
104
+ row.tokensPerMinute = row.totalTokens / minutes;
105
+ row.inputTokensPerMinute = row.inputTokens / minutes;
106
+ row.outputTokensPerMinute = row.outputTokens / minutes;
107
+ row.costUsdPerMinute = row.costUsd / minutes;
108
+ return row;
109
+ }
110
+ buckets(now) {
111
+ const bucketMs = THROUGHPUT_RETENTION_MS / THROUGHPUT_BUCKET_COUNT;
112
+ const endTs = Math.floor(now / bucketMs) * bucketMs + bucketMs;
113
+ const startTs = endTs - THROUGHPUT_RETENTION_MS;
114
+ const out = [];
115
+ for (let i = 0; i < THROUGHPUT_BUCKET_COUNT; i += 1) {
116
+ out.push({ startTs: startTs + i * bucketMs, requests: 0, tokens: 0 });
117
+ }
118
+ for (let i = this.head; i < this.samples.length; i += 1) {
119
+ const sample = this.samples[i];
120
+ if (sample.ts < startTs || sample.ts >= endTs) continue;
121
+ const bucket = out[Math.floor((sample.ts - startTs) / bucketMs)];
122
+ if (!bucket) continue;
123
+ bucket.requests += 1;
124
+ bucket.tokens += sample.inputTokens + sample.outputTokens + sample.cacheReadTokens + sample.cacheCreationTokens;
125
+ }
126
+ return out;
127
+ }
128
+ };
129
+ var sharedTracker = null;
130
+ function getSharedUsageThroughputTracker() {
131
+ if (!sharedTracker) sharedTracker = new UsageThroughputTracker();
132
+ return sharedTracker;
133
+ }
134
+ function setSharedUsageThroughputTracker(instance) {
135
+ sharedTracker = instance;
136
+ }
137
+ function __resetSharedUsageThroughputTrackerForTests() {
138
+ sharedTracker = null;
139
+ }
140
+
141
+ export {
142
+ THROUGHPUT_RETENTION_MS,
143
+ THROUGHPUT_SAMPLE_LIMIT,
144
+ THROUGHPUT_WINDOWS_MS,
145
+ THROUGHPUT_BUCKET_COUNT,
146
+ UsageThroughputTracker,
147
+ getSharedUsageThroughputTracker,
148
+ setSharedUsageThroughputTracker,
149
+ __resetSharedUsageThroughputTrackerForTests
150
+ };
@@ -10,12 +10,14 @@ var UsageRecorder = class {
10
10
  this.logger = logger;
11
11
  this.defer = options.defer ?? ((fn) => setTimeout(fn, 0));
12
12
  this.onRecord = options.onRecord;
13
+ this.onEvent = options.onEvent;
13
14
  }
14
15
  store;
15
16
  pricing;
16
17
  logger;
17
18
  defer;
18
19
  onRecord;
20
+ onEvent;
19
21
  /**
20
22
  * Record one LLM request. Returns immediately — the actual store insert is
21
23
  * deferred so the caller's response path is unblocked.
@@ -85,6 +87,12 @@ var UsageRecorder = class {
85
87
  ...input.cacheKeySource !== void 0 ? { cacheKeySource: input.cacheKeySource } : {},
86
88
  ...input.cacheKeyInjected !== void 0 ? { cacheKeyInjected: input.cacheKeyInjected } : {}
87
89
  };
90
+ if (this.onEvent) {
91
+ try {
92
+ this.onEvent(row, Date.now());
93
+ } catch {
94
+ }
95
+ }
88
96
  try {
89
97
  return await this.store.insert(row);
90
98
  } catch (err) {
@@ -33,9 +33,11 @@ import {
33
33
  captureCallerIdentity
34
34
  } from "./chunk-XX6NQJMA.js";
35
35
  import {
36
+ MATCH_TEXT_PER_MESSAGE_CAP,
36
37
  collectMatchText,
37
38
  deriveGatewaySessionKey,
38
- deriveSubscriptionSessionKey
39
+ deriveSubscriptionSessionKey,
40
+ flattenMatchText
39
41
  } from "./chunk-GFIZ6LQI.js";
40
42
  import {
41
43
  emitWebhookEvent
@@ -104,7 +106,7 @@ import {
104
106
  } from "./chunk-H5JUT3KV.js";
105
107
  import {
106
108
  beginAuditCapture
107
- } from "./chunk-7C7DET6S.js";
109
+ } from "./chunk-GXEZ2R3E.js";
108
110
  import {
109
111
  convertAnthropicToOpenAI,
110
112
  convertOpenAIToAnthropic
@@ -3664,6 +3666,7 @@ function normalizeAudit(raw) {
3664
3666
  retentionDays: Math.trunc(
3665
3667
  clampNumber(a?.retentionDays, 1, 365, DEFAULT_AUDIT_CONFIG.retentionDays)
3666
3668
  ),
3669
+ compactStreamingBodies: a?.compactStreamingBodies === true,
3667
3670
  trustForwardedFor: a?.trustForwardedFor === true
3668
3671
  };
3669
3672
  }
@@ -4131,8 +4134,62 @@ function mergeServerConfig(current, patch) {
4131
4134
  });
4132
4135
  }
4133
4136
 
4137
+ // src/outbound-api/auditSessionKey.ts
4138
+ import { createHash as createHash2 } from "crypto";
4139
+ var EXPLICIT_ID_SOURCES = /* @__PURE__ */ new Set([
4140
+ "session-header",
4141
+ "thread-header",
4142
+ "body-session-id",
4143
+ "body-thread-id",
4144
+ "prompt-cache-key"
4145
+ ]);
4146
+ var ANTHROPIC_SESSION_RE = /_session_([A-Za-z0-9][A-Za-z0-9_-]{7,})/;
4147
+ var DIGEST_HEX = 32;
4148
+ function stableDigest(value) {
4149
+ return createHash2("sha256").update(value, "utf8").digest("hex").slice(0, DIGEST_HEX);
4150
+ }
4151
+ function asObject(value) {
4152
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
4153
+ }
4154
+ function anthropicSessionId(body) {
4155
+ const userId = asObject(body["metadata"])?.["user_id"];
4156
+ if (typeof userId !== "string" || !userId) return void 0;
4157
+ const matched = ANTHROPIC_SESSION_RE.exec(userId);
4158
+ return matched?.[1];
4159
+ }
4160
+ function anthropicContentFingerprint(body) {
4161
+ const messages = body["messages"];
4162
+ if (!Array.isArray(messages) || messages.length === 0) return void 0;
4163
+ const parts = [];
4164
+ const system = flattenMatchText(body["system"]).trim();
4165
+ if (system) parts.push(system.slice(0, MATCH_TEXT_PER_MESSAGE_CAP));
4166
+ let firstUser = "";
4167
+ for (const entry of messages) {
4168
+ const message = asObject(entry);
4169
+ if (!message || message["role"] !== "user") continue;
4170
+ const text = flattenMatchText(message["content"]).trim();
4171
+ if (!text) continue;
4172
+ firstUser = text.slice(0, MATCH_TEXT_PER_MESSAGE_CAP);
4173
+ break;
4174
+ }
4175
+ if (parts.length === 0 && !firstUser) return void 0;
4176
+ return `${parts.join("\0")}\0${firstUser}`;
4177
+ }
4178
+ function deriveAuditSessionKey(body, headers = {}, options = {}) {
4179
+ const anthropicSession = anthropicSessionId(body);
4180
+ if (anthropicSession) return stableDigest(`anthropic-session\0${anthropicSession}`);
4181
+ const gateway = deriveGatewaySessionKey(body, headers, {
4182
+ ...options.fallbackKey !== void 0 ? { fallbackKey: options.fallbackKey } : {},
4183
+ ...options.endpoint !== void 0 ? { endpoint: options.endpoint } : {}
4184
+ });
4185
+ if (EXPLICIT_ID_SOURCES.has(gateway.source)) return gateway.key;
4186
+ const fingerprint = anthropicContentFingerprint(body);
4187
+ if (fingerprint) return stableDigest(`anthropic-content\0${fingerprint}`);
4188
+ return gateway.key;
4189
+ }
4190
+
4134
4191
  // src/outbound-api/outboundApiKeyAuth.ts
4135
- import { createHash as createHash2, randomBytes as randomBytes4 } from "crypto";
4192
+ import { createHash as createHash3, randomBytes as randomBytes4 } from "crypto";
4136
4193
 
4137
4194
  // src/outbound-api/keyPolicy.ts
4138
4195
  var DAY_MS = 864e5;
@@ -4197,7 +4254,7 @@ function generateSecret() {
4197
4254
  return KEY_PREFIX + randomBase62(SECRET_BYTES);
4198
4255
  }
4199
4256
  function hashKey(secret) {
4200
- return createHash2("sha256").update(secret, "utf8").digest("hex");
4257
+ return createHash3("sha256").update(secret, "utf8").digest("hex");
4201
4258
  }
4202
4259
  function keyPrefix(secret) {
4203
4260
  return secret.slice(0, PREFIX_LEN);
@@ -4525,7 +4582,7 @@ var OutboundRateLimiter = class {
4525
4582
  function asArray(v) {
4526
4583
  return Array.isArray(v) ? v : [];
4527
4584
  }
4528
- function asObject(v) {
4585
+ function asObject2(v) {
4529
4586
  return v !== null && typeof v === "object" && !Array.isArray(v) ? v : null;
4530
4587
  }
4531
4588
  function last(arr) {
@@ -4533,13 +4590,13 @@ function last(arr) {
4533
4590
  }
4534
4591
  function isUserMessageAnthropic(body) {
4535
4592
  const messages = asArray(body["messages"]);
4536
- const lastMsg = asObject(last(messages));
4593
+ const lastMsg = asObject2(last(messages));
4537
4594
  if (!lastMsg || lastMsg["role"] !== "user") return false;
4538
4595
  const content = lastMsg["content"];
4539
4596
  if (typeof content === "string") return true;
4540
4597
  if (Array.isArray(content)) {
4541
4598
  const hasToolResult = content.some(
4542
- (block) => asObject(block)?.["type"] === "tool_result"
4599
+ (block) => asObject2(block)?.["type"] === "tool_result"
4543
4600
  );
4544
4601
  return !hasToolResult;
4545
4602
  }
@@ -4549,7 +4606,7 @@ function isUserMessageResponses(body) {
4549
4606
  const input = body["input"];
4550
4607
  if (typeof input === "string") return input.length > 0;
4551
4608
  if (Array.isArray(input)) {
4552
- const lastItem = asObject(last(input));
4609
+ const lastItem = asObject2(last(input));
4553
4610
  if (!lastItem) return false;
4554
4611
  if (lastItem["type"] === "function_call_output") return false;
4555
4612
  return lastItem["role"] === "user";
@@ -4558,22 +4615,22 @@ function isUserMessageResponses(body) {
4558
4615
  }
4559
4616
  function isUserMessageChat(body) {
4560
4617
  const messages = asArray(body["messages"]);
4561
- const lastMsg = asObject(last(messages));
4618
+ const lastMsg = asObject2(last(messages));
4562
4619
  if (!lastMsg) return false;
4563
4620
  return lastMsg["role"] === "user";
4564
4621
  }
4565
4622
  function isUserMessageGemini(body) {
4566
4623
  const contents = asArray(body["contents"]);
4567
- const lastContent = asObject(last(contents));
4624
+ const lastContent = asObject2(last(contents));
4568
4625
  if (!lastContent || lastContent["role"] !== "user") return false;
4569
4626
  const parts = asArray(lastContent["parts"]);
4570
4627
  const hasFunctionResponse = parts.some(
4571
- (part) => asObject(part)?.["functionResponse"] !== void 0
4628
+ (part) => asObject2(part)?.["functionResponse"] !== void 0
4572
4629
  );
4573
4630
  return !hasFunctionResponse;
4574
4631
  }
4575
4632
  function isUserMessageRequest(endpoint, parsedBody) {
4576
- const body = asObject(parsedBody);
4633
+ const body = asObject2(parsedBody);
4577
4634
  if (!body) return false;
4578
4635
  switch (endpoint) {
4579
4636
  case "messages":
@@ -4723,7 +4780,7 @@ var UserMessageSerialQueue = class {
4723
4780
  };
4724
4781
 
4725
4782
  // src/outbound-api/voucher.ts
4726
- import { createHash as createHash3 } from "crypto";
4783
+ import { createHash as createHash4 } from "crypto";
4727
4784
  var CODE_ENTROPY_CHARS = 32;
4728
4785
  var CODE_PREFIX = "CC_";
4729
4786
  var DISPLAY_PREFIX_LEN = 8;
@@ -4732,7 +4789,7 @@ function generateVoucherCode() {
4732
4789
  return CODE_PREFIX + randomBase62(CODE_ENTROPY_CHARS);
4733
4790
  }
4734
4791
  function hashVoucherCode(code) {
4735
- return createHash3("sha256").update(code, "utf8").digest("hex");
4792
+ return createHash4("sha256").update(code, "utf8").digest("hex");
4736
4793
  }
4737
4794
  function voucherCodePrefix(code) {
4738
4795
  return code.slice(0, DISPLAY_PREFIX_LEN);
@@ -5185,7 +5242,13 @@ async function handleOutboundRequest(req, res, deps, config, rateLimiter, serial
5185
5242
  writeJsonError(res, 400, "Invalid JSON in request body");
5186
5243
  return;
5187
5244
  }
5188
- if (audit) audit.setRequestBody(rawBody);
5245
+ if (audit) {
5246
+ audit.setRequestBody(rawBody);
5247
+ audit.sessionKey = deriveAuditSessionKey(parsedBody, req.headers, {
5248
+ fallbackKey: verified.id,
5249
+ endpoint
5250
+ });
5251
+ }
5189
5252
  if (endpoint === "gemini" && typeof parsedBody["model"] !== "string") {
5190
5253
  const urlModel = extractGeminiModelFromUrl(req.url);
5191
5254
  if (urlModel) parsedBody["model"] = urlModel;
@@ -11,6 +11,156 @@ import {
11
11
 
12
12
  // src/outbound-api/auditCapture.ts
13
13
  import { randomUUID } from "crypto";
14
+
15
+ // src/outbound-api/auditSseCompact.ts
16
+ var MERGED_FRAMES_FIELD = "_omnicrossMergedFrames";
17
+ function asObject(value) {
18
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
19
+ }
20
+ function scalar(value) {
21
+ return typeof value === "string" || typeof value === "number" ? String(value) : "";
22
+ }
23
+ function classify(payload) {
24
+ const type = scalar(payload["type"]);
25
+ if (type === "content_block_delta") {
26
+ const delta = asObject(payload["delta"]);
27
+ const deltaType = scalar(delta?.["type"]);
28
+ const field = deltaType === "text_delta" ? "text" : deltaType === "thinking_delta" ? "thinking" : deltaType === "input_json_delta" ? "partial_json" : null;
29
+ if (!delta || !field || typeof delta[field] !== "string") return null;
30
+ return {
31
+ channel: "anthropic:" + scalar(payload["index"]) + ":" + deltaType,
32
+ text: delta[field],
33
+ payload,
34
+ textPath: ["delta", field]
35
+ };
36
+ }
37
+ if (type.endsWith(".delta") && typeof payload["delta"] === "string") {
38
+ const channel = [
39
+ "responses",
40
+ type,
41
+ scalar(payload["item_id"]),
42
+ scalar(payload["output_index"]),
43
+ scalar(payload["content_index"])
44
+ ].join(":");
45
+ return { channel, text: payload["delta"], payload, textPath: ["delta"] };
46
+ }
47
+ const choices = payload["choices"];
48
+ if (Array.isArray(choices) && choices.length === 1) {
49
+ const choice = asObject(choices[0]);
50
+ const delta = asObject(choice?.["delta"]);
51
+ if (choice && delta && typeof delta["content"] === "string" && choice["finish_reason"] == null && delta["tool_calls"] === void 0 && delta["function_call"] === void 0) {
52
+ return {
53
+ channel: "chat:" + scalar(choice["index"]),
54
+ text: delta["content"],
55
+ payload,
56
+ textPath: ["choices", "0", "delta", "content"]
57
+ };
58
+ }
59
+ }
60
+ return null;
61
+ }
62
+ function withMergedText(frame, text, mergedCount) {
63
+ const clone = JSON.parse(JSON.stringify(frame.payload));
64
+ let cursor = clone;
65
+ for (let i = 0; i < frame.textPath.length - 1; i += 1) {
66
+ const step = frame.textPath[i];
67
+ const next = Array.isArray(cursor) ? cursor[Number(step)] : cursor[step];
68
+ if (next === null || typeof next !== "object") return clone;
69
+ cursor = next;
70
+ }
71
+ const leaf = frame.textPath[frame.textPath.length - 1];
72
+ if (Array.isArray(cursor)) cursor[Number(leaf)] = text;
73
+ else cursor[leaf] = text;
74
+ clone[MERGED_FRAMES_FIELD] = mergedCount;
75
+ return clone;
76
+ }
77
+ function splitBlocks(text, nl) {
78
+ const blocks = [];
79
+ let head = [];
80
+ let data = null;
81
+ let raw = [];
82
+ const flush = () => {
83
+ if (raw.length === 0) return;
84
+ blocks.push({ head, data, raw: raw.join(nl) });
85
+ head = [];
86
+ data = null;
87
+ raw = [];
88
+ };
89
+ for (const line of text.split(nl)) {
90
+ if (line.trim() === "") {
91
+ flush();
92
+ continue;
93
+ }
94
+ raw.push(line);
95
+ if (line.startsWith("data:")) {
96
+ const payload = line.slice("data:".length).trimStart();
97
+ data = data === null ? payload : data + nl + payload;
98
+ } else {
99
+ head.push(line);
100
+ }
101
+ }
102
+ flush();
103
+ return blocks;
104
+ }
105
+ function compactSseBody(text) {
106
+ if (!text || !text.includes("data:")) return text;
107
+ const nl = "\n";
108
+ const blocks = splitBlocks(text, nl);
109
+ if (blocks.length === 0) return text;
110
+ const out = [];
111
+ let runFrame = null;
112
+ let runBlock = null;
113
+ let runText = "";
114
+ let runCount = 0;
115
+ let merged = 0;
116
+ const flushRun = () => {
117
+ if (runFrame === null || runBlock === null) return;
118
+ if (runCount === 1) {
119
+ out.push(runBlock.raw);
120
+ } else {
121
+ const payload = withMergedText(runFrame, runText, runCount);
122
+ out.push([...runBlock.head, "data: " + JSON.stringify(payload)].join(nl));
123
+ merged += runCount;
124
+ }
125
+ runFrame = null;
126
+ runBlock = null;
127
+ runText = "";
128
+ runCount = 0;
129
+ };
130
+ for (const block of blocks) {
131
+ let frame = null;
132
+ if (block.data !== null && block.data !== "[DONE]") {
133
+ try {
134
+ const parsed = asObject(JSON.parse(block.data));
135
+ if (parsed) frame = classify(parsed);
136
+ } catch {
137
+ frame = null;
138
+ }
139
+ }
140
+ if (frame === null) {
141
+ flushRun();
142
+ out.push(block.raw);
143
+ continue;
144
+ }
145
+ const open = runFrame;
146
+ if (open !== null && open.channel === frame.channel) {
147
+ runText += frame.text;
148
+ runCount += 1;
149
+ continue;
150
+ }
151
+ flushRun();
152
+ runFrame = frame;
153
+ runBlock = block;
154
+ runText = frame.text;
155
+ runCount = 1;
156
+ }
157
+ flushRun();
158
+ if (merged === 0) return text;
159
+ const trailer = /\s$/.test(text) ? nl + nl : "";
160
+ return out.join(nl + nl) + trailer;
161
+ }
162
+
163
+ // src/outbound-api/auditCapture.ts
14
164
  function completeUtf8PrefixLength(buf) {
15
165
  if (buf.length === 0) return 0;
16
166
  let leadIndex = buf.length - 1;
@@ -112,15 +262,20 @@ function beginAuditCapture(req, res, now) {
112
262
  if (!record.provider && usage.provider) record.provider = usage.provider;
113
263
  }
114
264
  if (ctx.error) record.error = redactAuditText(ctx.error);
265
+ if (ctx.sessionKey) record.sessionKey = ctx.sessionKey;
115
266
  if (config.captureBodies) {
116
267
  if (requestBody != null && requestBody.length > 0) {
117
268
  record.requestBody = requestBody;
118
269
  }
119
270
  if (responseChunks.length > 0) {
120
271
  const captured = Buffer.concat(responseChunks, responseBytes);
121
- const body = decodeCapturedBody(captured, responseTruncated);
272
+ const decoded = decodeCapturedBody(captured, responseTruncated);
273
+ const body = config.compactStreamingBodies ? compactSseBody(decoded) : decoded;
122
274
  record.responseBody = truncateToBytes(redactAuditText(body), config.maxBodyBytes);
123
275
  }
276
+ if (record.requestBody !== void 0 || record.responseBody !== void 0) {
277
+ record.hasBody = true;
278
+ }
124
279
  }
125
280
  recordAudit(record);
126
281
  } catch {