@llmops/app 0.3.2 → 0.3.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/dist/index.cjs CHANGED
@@ -16613,12 +16613,45 @@ const createGatewayAdapterMiddleware = () => {
16613
16613
  */
16614
16614
  function createStreamingCostExtractor() {
16615
16615
  let extractedUsage = null;
16616
+ let extractedHookResults = void 0;
16616
16617
  let buffer = "";
16617
16618
  let resolveUsage;
16618
16619
  const usagePromise = new Promise((resolve) => {
16619
16620
  resolveUsage = resolve;
16620
16621
  });
16621
16622
  const decoder = new TextDecoder();
16623
+ /**
16624
+ * Parse an SSE message and extract usage/hook_results
16625
+ */
16626
+ function parseSSEMessage(message) {
16627
+ const trimmed = message.trim();
16628
+ if (!trimmed) return;
16629
+ const lines = trimmed.split("\n");
16630
+ let eventType = null;
16631
+ let dataLine = null;
16632
+ for (const line of lines) if (line.startsWith("event:")) eventType = line.slice(6).trim();
16633
+ else if (line.startsWith("data:")) dataLine = line.slice(5).trim();
16634
+ if (dataLine === "[DONE]") return;
16635
+ if (!dataLine) return;
16636
+ try {
16637
+ const parsed = JSON.parse(dataLine);
16638
+ if (eventType === "hook_results" || parsed.hook_results) {
16639
+ const hookData = parsed.hook_results || parsed;
16640
+ if (hookData.before_request_hooks || hookData.after_request_hooks) extractedHookResults = {
16641
+ before_request_hooks: hookData.before_request_hooks,
16642
+ after_request_hooks: hookData.after_request_hooks
16643
+ };
16644
+ }
16645
+ const usageData = parsed;
16646
+ if (usageData.usage) extractedUsage = {
16647
+ promptTokens: usageData.usage.prompt_tokens ?? 0,
16648
+ completionTokens: usageData.usage.completion_tokens ?? 0,
16649
+ totalTokens: usageData.usage.total_tokens ?? 0,
16650
+ cachedTokens: usageData.usage.prompt_tokens_details?.cached_tokens,
16651
+ hookResults: extractedHookResults
16652
+ };
16653
+ } catch {}
16654
+ }
16622
16655
  return {
16623
16656
  stream: new TransformStream({
16624
16657
  transform(chunk, controller) {
@@ -16627,39 +16660,17 @@ function createStreamingCostExtractor() {
16627
16660
  buffer += text;
16628
16661
  const messages = buffer.split("\n\n");
16629
16662
  buffer = messages.pop() || "";
16630
- for (const message of messages) {
16631
- const trimmed = message.trim();
16632
- if (!trimmed) continue;
16633
- if (!trimmed.startsWith("data:")) continue;
16634
- const jsonPart = trimmed.slice(5).trim();
16635
- if (jsonPart === "[DONE]") continue;
16636
- try {
16637
- const parsed = JSON.parse(jsonPart);
16638
- if (parsed.usage) extractedUsage = {
16639
- promptTokens: parsed.usage.prompt_tokens ?? 0,
16640
- completionTokens: parsed.usage.completion_tokens ?? 0,
16641
- totalTokens: parsed.usage.total_tokens ?? 0,
16642
- cachedTokens: parsed.usage.prompt_tokens_details?.cached_tokens
16643
- };
16644
- } catch {}
16645
- }
16663
+ for (const message of messages) parseSSEMessage(message);
16646
16664
  },
16647
16665
  flush(controller) {
16648
- if (buffer.trim()) {
16649
- const trimmed = buffer.trim();
16650
- if (trimmed.startsWith("data:")) {
16651
- const jsonPart = trimmed.slice(5).trim();
16652
- if (jsonPart !== "[DONE]") try {
16653
- const parsed = JSON.parse(jsonPart);
16654
- if (parsed.usage) extractedUsage = {
16655
- promptTokens: parsed.usage.prompt_tokens ?? 0,
16656
- completionTokens: parsed.usage.completion_tokens ?? 0,
16657
- totalTokens: parsed.usage.total_tokens ?? 0,
16658
- cachedTokens: parsed.usage.prompt_tokens_details?.cached_tokens
16659
- };
16660
- } catch {}
16661
- }
16662
- }
16666
+ if (buffer.trim()) parseSSEMessage(buffer);
16667
+ if (extractedUsage && extractedHookResults) extractedUsage.hookResults = extractedHookResults;
16668
+ else if (!extractedUsage && extractedHookResults) extractedUsage = {
16669
+ promptTokens: 0,
16670
+ completionTokens: 0,
16671
+ totalTokens: 0,
16672
+ hookResults: extractedHookResults
16673
+ };
16663
16674
  resolveUsage(extractedUsage);
16664
16675
  }
16665
16676
  }),
@@ -16899,6 +16910,47 @@ var PricingProvider = class {
16899
16910
  };
16900
16911
  const pricingProvider = new PricingProvider();
16901
16912
  /**
16913
+ * Transform gateway hook results to our schema format for telemetry
16914
+ */
16915
+ function transformHookResultsToGuardrailResults(hookResults, wasBlocked) {
16916
+ if (!hookResults) return null;
16917
+ const beforeHooks = hookResults.before_request_hooks || [];
16918
+ const afterHooks = hookResults.after_request_hooks || [];
16919
+ if (beforeHooks.length === 0 && afterHooks.length === 0) return null;
16920
+ const results = [];
16921
+ let totalLatencyMs = 0;
16922
+ for (const hook of beforeHooks) {
16923
+ totalLatencyMs += hook.execution_time;
16924
+ for (const check$1 of hook.checks) results.push({
16925
+ checkId: check$1.id,
16926
+ functionId: check$1.id.split(".")[1] || check$1.id,
16927
+ hookType: "beforeRequestHook",
16928
+ verdict: check$1.verdict,
16929
+ latencyMs: check$1.execution_time
16930
+ });
16931
+ }
16932
+ for (const hook of afterHooks) {
16933
+ totalLatencyMs += hook.execution_time;
16934
+ for (const check$1 of hook.checks) results.push({
16935
+ checkId: check$1.id,
16936
+ functionId: check$1.id.split(".")[1] || check$1.id,
16937
+ hookType: "afterRequestHook",
16938
+ verdict: check$1.verdict,
16939
+ latencyMs: check$1.execution_time
16940
+ });
16941
+ }
16942
+ const anyFailed = results.some((r) => !r.verdict);
16943
+ let action;
16944
+ if (wasBlocked) action = "blocked";
16945
+ else if (anyFailed) action = "logged";
16946
+ else action = "allowed";
16947
+ return {
16948
+ results,
16949
+ action,
16950
+ totalLatencyMs
16951
+ };
16952
+ }
16953
+ /**
16902
16954
  * Creates cost tracking middleware that logs LLM requests with usage and cost data.
16903
16955
  *
16904
16956
  * Features:
@@ -16976,6 +17028,7 @@ function createCostTrackingMiddleware(config$1 = {}) {
16976
17028
  const { response: wrappedResponse, usagePromise } = wrapStreamingResponse(response);
16977
17029
  c.res = wrappedResponse;
16978
17030
  usagePromise.then(async (usage) => {
17031
+ const guardrailResults = usage?.hookResults ? transformHookResultsToGuardrailResults(usage.hookResults, statusCode === 446) : null;
16979
17032
  await processUsageAndLog({
16980
17033
  requestId,
16981
17034
  provider,
@@ -16994,6 +17047,7 @@ function createCostTrackingMiddleware(config$1 = {}) {
16994
17047
  totalTokens: usage.totalTokens,
16995
17048
  cachedTokens: usage.cachedTokens
16996
17049
  } : null,
17050
+ guardrailResults,
16997
17051
  tags: customTags,
16998
17052
  batchWriter,
16999
17053
  trackErrors,
@@ -17004,6 +17058,7 @@ function createCostTrackingMiddleware(config$1 = {}) {
17004
17058
  });
17005
17059
  } else {
17006
17060
  let usage = null;
17061
+ let guardrailResults = null;
17007
17062
  try {
17008
17063
  const responseBody = await response.clone().json();
17009
17064
  if (responseBody.usage) usage = {
@@ -17012,6 +17067,11 @@ function createCostTrackingMiddleware(config$1 = {}) {
17012
17067
  totalTokens: responseBody.usage.total_tokens || 0,
17013
17068
  cachedTokens: responseBody.usage.prompt_tokens_details?.cached_tokens
17014
17069
  };
17070
+ if (responseBody.hook_results) {
17071
+ const wasBlocked = statusCode === 446;
17072
+ guardrailResults = transformHookResultsToGuardrailResults(responseBody.hook_results, wasBlocked);
17073
+ if (guardrailResults) log(`Extracted guardrail results: ${guardrailResults.results.length} checks, action=${guardrailResults.action}`);
17074
+ }
17015
17075
  } catch {
17016
17076
  log("Failed to parse response body for usage");
17017
17077
  }
@@ -17028,6 +17088,7 @@ function createCostTrackingMiddleware(config$1 = {}) {
17028
17088
  latencyMs,
17029
17089
  isStreaming: false,
17030
17090
  usage,
17091
+ guardrailResults,
17031
17092
  tags: customTags,
17032
17093
  batchWriter,
17033
17094
  trackErrors,
@@ -17040,7 +17101,7 @@ function createCostTrackingMiddleware(config$1 = {}) {
17040
17101
  * Process usage data and log to batch writer
17041
17102
  */
17042
17103
  async function processUsageAndLog(params) {
17043
- const { requestId, provider, model, configId, variantId, environmentId, providerConfigId, endpoint, statusCode, latencyMs, isStreaming, usage, tags = {}, batchWriter, trackErrors, log } = params;
17104
+ const { requestId, provider, model, configId, variantId, environmentId, providerConfigId, endpoint, statusCode, latencyMs, isStreaming, usage, guardrailResults, tags = {}, batchWriter, trackErrors, log } = params;
17044
17105
  if (!trackErrors && statusCode >= 400) {
17045
17106
  log(`Skipping error response (${statusCode})`);
17046
17107
  return;
@@ -17091,7 +17152,8 @@ async function processUsageAndLog(params) {
17091
17152
  statusCode,
17092
17153
  latencyMs,
17093
17154
  isStreaming,
17094
- tags
17155
+ tags,
17156
+ guardrailResults: guardrailResults || null
17095
17157
  };
17096
17158
  batchWriter.enqueue(requestData);
17097
17159
  log(`Enqueued request ${requestId} for logging`);
package/dist/index.mjs CHANGED
@@ -16585,12 +16585,45 @@ const createGatewayAdapterMiddleware = () => {
16585
16585
  */
16586
16586
  function createStreamingCostExtractor() {
16587
16587
  let extractedUsage = null;
16588
+ let extractedHookResults = void 0;
16588
16589
  let buffer = "";
16589
16590
  let resolveUsage;
16590
16591
  const usagePromise = new Promise((resolve) => {
16591
16592
  resolveUsage = resolve;
16592
16593
  });
16593
16594
  const decoder = new TextDecoder();
16595
+ /**
16596
+ * Parse an SSE message and extract usage/hook_results
16597
+ */
16598
+ function parseSSEMessage(message) {
16599
+ const trimmed = message.trim();
16600
+ if (!trimmed) return;
16601
+ const lines = trimmed.split("\n");
16602
+ let eventType = null;
16603
+ let dataLine = null;
16604
+ for (const line of lines) if (line.startsWith("event:")) eventType = line.slice(6).trim();
16605
+ else if (line.startsWith("data:")) dataLine = line.slice(5).trim();
16606
+ if (dataLine === "[DONE]") return;
16607
+ if (!dataLine) return;
16608
+ try {
16609
+ const parsed = JSON.parse(dataLine);
16610
+ if (eventType === "hook_results" || parsed.hook_results) {
16611
+ const hookData = parsed.hook_results || parsed;
16612
+ if (hookData.before_request_hooks || hookData.after_request_hooks) extractedHookResults = {
16613
+ before_request_hooks: hookData.before_request_hooks,
16614
+ after_request_hooks: hookData.after_request_hooks
16615
+ };
16616
+ }
16617
+ const usageData = parsed;
16618
+ if (usageData.usage) extractedUsage = {
16619
+ promptTokens: usageData.usage.prompt_tokens ?? 0,
16620
+ completionTokens: usageData.usage.completion_tokens ?? 0,
16621
+ totalTokens: usageData.usage.total_tokens ?? 0,
16622
+ cachedTokens: usageData.usage.prompt_tokens_details?.cached_tokens,
16623
+ hookResults: extractedHookResults
16624
+ };
16625
+ } catch {}
16626
+ }
16594
16627
  return {
16595
16628
  stream: new TransformStream({
16596
16629
  transform(chunk, controller) {
@@ -16599,39 +16632,17 @@ function createStreamingCostExtractor() {
16599
16632
  buffer += text;
16600
16633
  const messages = buffer.split("\n\n");
16601
16634
  buffer = messages.pop() || "";
16602
- for (const message of messages) {
16603
- const trimmed = message.trim();
16604
- if (!trimmed) continue;
16605
- if (!trimmed.startsWith("data:")) continue;
16606
- const jsonPart = trimmed.slice(5).trim();
16607
- if (jsonPart === "[DONE]") continue;
16608
- try {
16609
- const parsed = JSON.parse(jsonPart);
16610
- if (parsed.usage) extractedUsage = {
16611
- promptTokens: parsed.usage.prompt_tokens ?? 0,
16612
- completionTokens: parsed.usage.completion_tokens ?? 0,
16613
- totalTokens: parsed.usage.total_tokens ?? 0,
16614
- cachedTokens: parsed.usage.prompt_tokens_details?.cached_tokens
16615
- };
16616
- } catch {}
16617
- }
16635
+ for (const message of messages) parseSSEMessage(message);
16618
16636
  },
16619
16637
  flush(controller) {
16620
- if (buffer.trim()) {
16621
- const trimmed = buffer.trim();
16622
- if (trimmed.startsWith("data:")) {
16623
- const jsonPart = trimmed.slice(5).trim();
16624
- if (jsonPart !== "[DONE]") try {
16625
- const parsed = JSON.parse(jsonPart);
16626
- if (parsed.usage) extractedUsage = {
16627
- promptTokens: parsed.usage.prompt_tokens ?? 0,
16628
- completionTokens: parsed.usage.completion_tokens ?? 0,
16629
- totalTokens: parsed.usage.total_tokens ?? 0,
16630
- cachedTokens: parsed.usage.prompt_tokens_details?.cached_tokens
16631
- };
16632
- } catch {}
16633
- }
16634
- }
16638
+ if (buffer.trim()) parseSSEMessage(buffer);
16639
+ if (extractedUsage && extractedHookResults) extractedUsage.hookResults = extractedHookResults;
16640
+ else if (!extractedUsage && extractedHookResults) extractedUsage = {
16641
+ promptTokens: 0,
16642
+ completionTokens: 0,
16643
+ totalTokens: 0,
16644
+ hookResults: extractedHookResults
16645
+ };
16635
16646
  resolveUsage(extractedUsage);
16636
16647
  }
16637
16648
  }),
@@ -16871,6 +16882,47 @@ var PricingProvider = class {
16871
16882
  };
16872
16883
  const pricingProvider = new PricingProvider();
16873
16884
  /**
16885
+ * Transform gateway hook results to our schema format for telemetry
16886
+ */
16887
+ function transformHookResultsToGuardrailResults(hookResults, wasBlocked) {
16888
+ if (!hookResults) return null;
16889
+ const beforeHooks = hookResults.before_request_hooks || [];
16890
+ const afterHooks = hookResults.after_request_hooks || [];
16891
+ if (beforeHooks.length === 0 && afterHooks.length === 0) return null;
16892
+ const results = [];
16893
+ let totalLatencyMs = 0;
16894
+ for (const hook of beforeHooks) {
16895
+ totalLatencyMs += hook.execution_time;
16896
+ for (const check$1 of hook.checks) results.push({
16897
+ checkId: check$1.id,
16898
+ functionId: check$1.id.split(".")[1] || check$1.id,
16899
+ hookType: "beforeRequestHook",
16900
+ verdict: check$1.verdict,
16901
+ latencyMs: check$1.execution_time
16902
+ });
16903
+ }
16904
+ for (const hook of afterHooks) {
16905
+ totalLatencyMs += hook.execution_time;
16906
+ for (const check$1 of hook.checks) results.push({
16907
+ checkId: check$1.id,
16908
+ functionId: check$1.id.split(".")[1] || check$1.id,
16909
+ hookType: "afterRequestHook",
16910
+ verdict: check$1.verdict,
16911
+ latencyMs: check$1.execution_time
16912
+ });
16913
+ }
16914
+ const anyFailed = results.some((r) => !r.verdict);
16915
+ let action;
16916
+ if (wasBlocked) action = "blocked";
16917
+ else if (anyFailed) action = "logged";
16918
+ else action = "allowed";
16919
+ return {
16920
+ results,
16921
+ action,
16922
+ totalLatencyMs
16923
+ };
16924
+ }
16925
+ /**
16874
16926
  * Creates cost tracking middleware that logs LLM requests with usage and cost data.
16875
16927
  *
16876
16928
  * Features:
@@ -16948,6 +17000,7 @@ function createCostTrackingMiddleware(config$1 = {}) {
16948
17000
  const { response: wrappedResponse, usagePromise } = wrapStreamingResponse(response);
16949
17001
  c.res = wrappedResponse;
16950
17002
  usagePromise.then(async (usage) => {
17003
+ const guardrailResults = usage?.hookResults ? transformHookResultsToGuardrailResults(usage.hookResults, statusCode === 446) : null;
16951
17004
  await processUsageAndLog({
16952
17005
  requestId,
16953
17006
  provider,
@@ -16966,6 +17019,7 @@ function createCostTrackingMiddleware(config$1 = {}) {
16966
17019
  totalTokens: usage.totalTokens,
16967
17020
  cachedTokens: usage.cachedTokens
16968
17021
  } : null,
17022
+ guardrailResults,
16969
17023
  tags: customTags,
16970
17024
  batchWriter,
16971
17025
  trackErrors,
@@ -16976,6 +17030,7 @@ function createCostTrackingMiddleware(config$1 = {}) {
16976
17030
  });
16977
17031
  } else {
16978
17032
  let usage = null;
17033
+ let guardrailResults = null;
16979
17034
  try {
16980
17035
  const responseBody = await response.clone().json();
16981
17036
  if (responseBody.usage) usage = {
@@ -16984,6 +17039,11 @@ function createCostTrackingMiddleware(config$1 = {}) {
16984
17039
  totalTokens: responseBody.usage.total_tokens || 0,
16985
17040
  cachedTokens: responseBody.usage.prompt_tokens_details?.cached_tokens
16986
17041
  };
17042
+ if (responseBody.hook_results) {
17043
+ const wasBlocked = statusCode === 446;
17044
+ guardrailResults = transformHookResultsToGuardrailResults(responseBody.hook_results, wasBlocked);
17045
+ if (guardrailResults) log(`Extracted guardrail results: ${guardrailResults.results.length} checks, action=${guardrailResults.action}`);
17046
+ }
16987
17047
  } catch {
16988
17048
  log("Failed to parse response body for usage");
16989
17049
  }
@@ -17000,6 +17060,7 @@ function createCostTrackingMiddleware(config$1 = {}) {
17000
17060
  latencyMs,
17001
17061
  isStreaming: false,
17002
17062
  usage,
17063
+ guardrailResults,
17003
17064
  tags: customTags,
17004
17065
  batchWriter,
17005
17066
  trackErrors,
@@ -17012,7 +17073,7 @@ function createCostTrackingMiddleware(config$1 = {}) {
17012
17073
  * Process usage data and log to batch writer
17013
17074
  */
17014
17075
  async function processUsageAndLog(params) {
17015
- const { requestId, provider, model, configId, variantId, environmentId, providerConfigId, endpoint, statusCode, latencyMs, isStreaming, usage, tags = {}, batchWriter, trackErrors, log } = params;
17076
+ const { requestId, provider, model, configId, variantId, environmentId, providerConfigId, endpoint, statusCode, latencyMs, isStreaming, usage, guardrailResults, tags = {}, batchWriter, trackErrors, log } = params;
17016
17077
  if (!trackErrors && statusCode >= 400) {
17017
17078
  log(`Skipping error response (${statusCode})`);
17018
17079
  return;
@@ -17063,7 +17124,8 @@ async function processUsageAndLog(params) {
17063
17124
  statusCode,
17064
17125
  latencyMs,
17065
17126
  isStreaming,
17066
- tags
17127
+ tags,
17128
+ guardrailResults: guardrailResults || null
17067
17129
  };
17068
17130
  batchWriter.enqueue(requestData);
17069
17131
  log(`Enqueued request ${requestId} for logging`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llmops/app",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "LLMOps application with server and client",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -67,8 +67,8 @@
67
67
  "react-aria-components": "^1.13.0",
68
68
  "react-hook-form": "^7.68.0",
69
69
  "recharts": "^3.6.0",
70
- "@llmops/core": "^0.3.2",
71
- "@llmops/gateway": "^0.3.2"
70
+ "@llmops/core": "^0.3.3",
71
+ "@llmops/gateway": "^0.3.3"
72
72
  },
73
73
  "peerDependencies": {
74
74
  "react": "^19.2.1",