@llmops/app 0.6.3 → 0.6.6

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 (3) hide show
  1. package/dist/index.cjs +410 -77
  2. package/dist/index.mjs +409 -77
  3. package/package.json +4 -3
package/dist/index.cjs CHANGED
@@ -59,6 +59,8 @@ let hono_cors = require("hono/cors");
59
59
  let node_crypto = require("node:crypto");
60
60
  let __llmops_gateway = require("@llmops/gateway");
61
61
  __llmops_gateway = __toESM(__llmops_gateway);
62
+ let protobufjs = require("protobufjs");
63
+ protobufjs = __toESM(protobufjs);
62
64
  let node_process = require("node:process");
63
65
  let __llmops_core_db = require("@llmops/core/db");
64
66
  let better_auth = require("better-auth");
@@ -18287,6 +18289,7 @@ function createTraceBatchWriter(deps, config$1 = {}) {
18287
18289
  const batch = queue;
18288
18290
  queue = [];
18289
18291
  try {
18292
+ __llmops_core.logger.info(`[TraceBatchWriter] Flushing ${batch.length} items`);
18290
18293
  log(`[TraceBatchWriter] Flushing ${batch.length} items`);
18291
18294
  const traceMap = /* @__PURE__ */ new Map();
18292
18295
  for (const item of batch) {
@@ -18320,6 +18323,7 @@ function createTraceBatchWriter(deps, config$1 = {}) {
18320
18323
  if (allSpans.length > 0) await deps.batchInsertSpans(allSpans);
18321
18324
  const allEvents = batch.flatMap((item) => item.events ?? []);
18322
18325
  if (allEvents.length > 0) await deps.batchInsertSpanEvents(allEvents);
18326
+ __llmops_core.logger.info(`[TraceBatchWriter] Flushed ${traceMap.size} traces, ${allSpans.length} spans, ${allEvents.length} events`);
18323
18327
  log(`[TraceBatchWriter] Flushed ${traceMap.size} traces, ${allSpans.length} spans, ${allEvents.length} events`);
18324
18328
  } catch (error$47) {
18325
18329
  const errorMsg = error$47 instanceof Error ? error$47.message : String(error$47);
@@ -18808,6 +18812,317 @@ app$4.use("*", (0, hono_pretty_json.prettyJSON)()).get("/health", async (c) => {
18808
18812
  });
18809
18813
  var genai_default = app$4;
18810
18814
 
18815
+ //#endregion
18816
+ //#region src/server/handlers/otlp/decode.ts
18817
+ /**
18818
+ * OTLP Protobuf Decoder
18819
+ *
18820
+ * Decodes binary protobuf ExportTraceServiceRequest into the same JSON
18821
+ * structure the OTLP handler already expects. Uses an embedded protobufjs
18822
+ * JSON descriptor — no .proto files needed at runtime.
18823
+ */
18824
+ const protoDescriptor = { nested: {
18825
+ ExportTraceServiceRequest: { fields: { resourceSpans: {
18826
+ rule: "repeated",
18827
+ type: "ResourceSpans",
18828
+ id: 1
18829
+ } } },
18830
+ ResourceSpans: { fields: {
18831
+ resource: {
18832
+ type: "Resource",
18833
+ id: 1
18834
+ },
18835
+ scopeSpans: {
18836
+ rule: "repeated",
18837
+ type: "ScopeSpans",
18838
+ id: 2
18839
+ },
18840
+ instrumentationLibrarySpans: {
18841
+ rule: "repeated",
18842
+ type: "ScopeSpans",
18843
+ id: 1e3
18844
+ }
18845
+ } },
18846
+ Resource: { fields: { attributes: {
18847
+ rule: "repeated",
18848
+ type: "KeyValue",
18849
+ id: 1
18850
+ } } },
18851
+ ScopeSpans: { fields: {
18852
+ scope: {
18853
+ type: "InstrumentationScope",
18854
+ id: 1
18855
+ },
18856
+ spans: {
18857
+ rule: "repeated",
18858
+ type: "Span",
18859
+ id: 2
18860
+ }
18861
+ } },
18862
+ InstrumentationScope: { fields: {
18863
+ name: {
18864
+ type: "string",
18865
+ id: 1
18866
+ },
18867
+ version: {
18868
+ type: "string",
18869
+ id: 2
18870
+ }
18871
+ } },
18872
+ Span: { fields: {
18873
+ traceId: {
18874
+ type: "bytes",
18875
+ id: 1
18876
+ },
18877
+ spanId: {
18878
+ type: "bytes",
18879
+ id: 2
18880
+ },
18881
+ traceState: {
18882
+ type: "string",
18883
+ id: 3
18884
+ },
18885
+ parentSpanId: {
18886
+ type: "bytes",
18887
+ id: 4
18888
+ },
18889
+ name: {
18890
+ type: "string",
18891
+ id: 5
18892
+ },
18893
+ kind: {
18894
+ type: "SpanKind",
18895
+ id: 6
18896
+ },
18897
+ startTimeUnixNano: {
18898
+ type: "fixed64",
18899
+ id: 7
18900
+ },
18901
+ endTimeUnixNano: {
18902
+ type: "fixed64",
18903
+ id: 8
18904
+ },
18905
+ attributes: {
18906
+ rule: "repeated",
18907
+ type: "KeyValue",
18908
+ id: 9
18909
+ },
18910
+ events: {
18911
+ rule: "repeated",
18912
+ type: "SpanEvent",
18913
+ id: 10
18914
+ },
18915
+ status: {
18916
+ type: "Status",
18917
+ id: 15
18918
+ }
18919
+ } },
18920
+ SpanKind: { values: {
18921
+ SPAN_KIND_UNSPECIFIED: 0,
18922
+ SPAN_KIND_INTERNAL: 1,
18923
+ SPAN_KIND_SERVER: 2,
18924
+ SPAN_KIND_CLIENT: 3,
18925
+ SPAN_KIND_PRODUCER: 4,
18926
+ SPAN_KIND_CONSUMER: 5
18927
+ } },
18928
+ SpanEvent: { fields: {
18929
+ timeUnixNano: {
18930
+ type: "fixed64",
18931
+ id: 1
18932
+ },
18933
+ name: {
18934
+ type: "string",
18935
+ id: 2
18936
+ },
18937
+ attributes: {
18938
+ rule: "repeated",
18939
+ type: "KeyValue",
18940
+ id: 3
18941
+ }
18942
+ } },
18943
+ Status: { fields: {
18944
+ message: {
18945
+ type: "string",
18946
+ id: 2
18947
+ },
18948
+ code: {
18949
+ type: "StatusCode",
18950
+ id: 3
18951
+ }
18952
+ } },
18953
+ StatusCode: { values: {
18954
+ STATUS_CODE_UNSET: 0,
18955
+ STATUS_CODE_OK: 1,
18956
+ STATUS_CODE_ERROR: 2
18957
+ } },
18958
+ KeyValue: { fields: {
18959
+ key: {
18960
+ type: "string",
18961
+ id: 1
18962
+ },
18963
+ value: {
18964
+ type: "AnyValue",
18965
+ id: 2
18966
+ }
18967
+ } },
18968
+ AnyValue: {
18969
+ fields: {
18970
+ stringValue: {
18971
+ type: "string",
18972
+ id: 1
18973
+ },
18974
+ boolValue: {
18975
+ type: "bool",
18976
+ id: 2
18977
+ },
18978
+ intValue: {
18979
+ type: "int64",
18980
+ id: 3
18981
+ },
18982
+ doubleValue: {
18983
+ type: "double",
18984
+ id: 4
18985
+ },
18986
+ arrayValue: {
18987
+ type: "ArrayValue",
18988
+ id: 5
18989
+ },
18990
+ kvlistValue: {
18991
+ type: "KeyValueList",
18992
+ id: 6
18993
+ },
18994
+ bytesValue: {
18995
+ type: "bytes",
18996
+ id: 7
18997
+ }
18998
+ },
18999
+ oneofs: { value: { oneof: [
19000
+ "stringValue",
19001
+ "boolValue",
19002
+ "intValue",
19003
+ "doubleValue",
19004
+ "arrayValue",
19005
+ "kvlistValue",
19006
+ "bytesValue"
19007
+ ] } }
19008
+ },
19009
+ ArrayValue: { fields: { values: {
19010
+ rule: "repeated",
19011
+ type: "AnyValue",
19012
+ id: 1
19013
+ } } },
19014
+ KeyValueList: { fields: { values: {
19015
+ rule: "repeated",
19016
+ type: "KeyValue",
19017
+ id: 1
19018
+ } } }
19019
+ } };
19020
+ let _root = null;
19021
+ let _RequestType = null;
19022
+ function getRequestType() {
19023
+ if (!_RequestType) {
19024
+ _root = protobufjs.default.Root.fromJSON(protoDescriptor);
19025
+ _RequestType = _root.lookupType("ExportTraceServiceRequest");
19026
+ }
19027
+ return _RequestType;
19028
+ }
19029
+ function bytesToHex(bytes) {
19030
+ return Buffer.from(bytes).toString("hex");
19031
+ }
19032
+ function isZeroBytes(bytes) {
19033
+ for (let i = 0; i < bytes.length; i++) if (bytes[i] !== 0) return false;
19034
+ return true;
19035
+ }
19036
+ /**
19037
+ * Convert a protobufjs Long / number / string to a nanosecond string.
19038
+ * protobufjs decodes fixed64 as Long when longs are not forced to number.
19039
+ */
19040
+ function toNanoString(val) {
19041
+ if (val == null) return "0";
19042
+ if (typeof val === "object" && val !== null && "toString" in val) return val.toString();
19043
+ return String(val);
19044
+ }
19045
+ /**
19046
+ * Normalise an AnyValue from decoded protobuf into the OTLP JSON shape.
19047
+ */
19048
+ function normaliseAnyValue(av) {
19049
+ const out = {};
19050
+ if (av.stringValue !== void 0 && av.stringValue !== "") out.stringValue = av.stringValue;
19051
+ if (av.boolValue !== void 0 && av.boolValue !== false) out.boolValue = av.boolValue;
19052
+ if (av.intValue !== void 0) {
19053
+ const v = av.intValue;
19054
+ out.intValue = typeof v === "object" && v !== null && "toNumber" in v ? v.toNumber() : Number(v);
19055
+ }
19056
+ if (av.doubleValue !== void 0 && av.doubleValue !== 0) out.doubleValue = av.doubleValue;
19057
+ if (av.arrayValue !== void 0 && av.arrayValue !== null) out.arrayValue = { values: (av.arrayValue.values ?? []).map((v) => normaliseAnyValue(v)) };
19058
+ if (av.kvlistValue !== void 0 && av.kvlistValue !== null) out.kvlistValue = { values: (av.kvlistValue.values ?? []).map((kv) => normaliseKeyValue(kv)) };
19059
+ if (av.bytesValue !== void 0) out.bytesValue = bytesToHex(av.bytesValue);
19060
+ return out;
19061
+ }
19062
+ function normaliseKeyValue(kv) {
19063
+ return {
19064
+ key: kv.key,
19065
+ value: normaliseAnyValue(kv.value ?? {})
19066
+ };
19067
+ }
19068
+ /**
19069
+ * Decode an OTLP protobuf-encoded ExportTraceServiceRequest binary into
19070
+ * the same JSON structure the handler expects.
19071
+ *
19072
+ * - bytes fields (trace_id, span_id, parent_span_id) → hex strings
19073
+ * - fixed64 nano timestamps → string (for nanoToDate())
19074
+ * - Long intValue → number
19075
+ * - deprecated instrumentationLibrarySpans merged into scopeSpans
19076
+ */
19077
+ function decodeOtlpProtobuf(buffer) {
19078
+ return { resourceSpans: (getRequestType().decode(buffer).resourceSpans ?? []).map((rs) => {
19079
+ const rawResource = rs.resource;
19080
+ return {
19081
+ resource: rawResource ? { attributes: (rawResource.attributes ?? []).map(normaliseKeyValue) } : void 0,
19082
+ scopeSpans: [...rs.scopeSpans ?? [], ...rs.instrumentationLibrarySpans ?? []].map((ss) => {
19083
+ const rawScope = ss.scope;
19084
+ const spans = (ss.spans ?? []).map((s) => {
19085
+ const traceIdBytes = s.traceId;
19086
+ const spanIdBytes = s.spanId;
19087
+ const parentSpanIdBytes = s.parentSpanId;
19088
+ const traceId = traceIdBytes ? bytesToHex(traceIdBytes) : "";
19089
+ const spanId = spanIdBytes ? bytesToHex(spanIdBytes) : "";
19090
+ const parentSpanId = parentSpanIdBytes && parentSpanIdBytes.length > 0 && !isZeroBytes(parentSpanIdBytes) ? bytesToHex(parentSpanIdBytes) : void 0;
19091
+ const attributes = (s.attributes ?? []).map(normaliseKeyValue);
19092
+ const events = (s.events ?? []).map((e) => ({
19093
+ name: e.name ?? "",
19094
+ timeUnixNano: toNanoString(e.timeUnixNano),
19095
+ attributes: (e.attributes ?? []).map(normaliseKeyValue)
19096
+ }));
19097
+ const rawStatus = s.status;
19098
+ return {
19099
+ traceId,
19100
+ spanId,
19101
+ parentSpanId,
19102
+ name: s.name ?? "",
19103
+ kind: s.kind,
19104
+ startTimeUnixNano: toNanoString(s.startTimeUnixNano),
19105
+ endTimeUnixNano: s.endTimeUnixNano ? toNanoString(s.endTimeUnixNano) : void 0,
19106
+ attributes,
19107
+ events: events.length > 0 ? events : void 0,
19108
+ status: rawStatus ? {
19109
+ code: rawStatus.code,
19110
+ message: rawStatus.message
19111
+ } : void 0
19112
+ };
19113
+ });
19114
+ return {
19115
+ scope: rawScope ? {
19116
+ name: rawScope.name,
19117
+ version: rawScope.version
19118
+ } : void 0,
19119
+ spans
19120
+ };
19121
+ })
19122
+ };
19123
+ }) };
19124
+ }
19125
+
18811
19126
  //#endregion
18812
19127
  //#region src/server/handlers/otlp/index.ts
18813
19128
  /**
@@ -18876,9 +19191,10 @@ function extractTypedFields(attrs) {
18876
19191
  const pricingProvider = (0, __llmops_core.getDefaultPricingProvider)();
18877
19192
  /**
18878
19193
  * OTLP ingestion endpoint
18879
- * Accepts OTLP JSON (ExportTraceServiceRequest) format
19194
+ * Accepts OTLP JSON and Protobuf (ExportTraceServiceRequest) formats
18880
19195
  */
18881
19196
  const app$3 = new hono.Hono().post("/v1/traces", async (c) => {
19197
+ __llmops_core.logger.info(`[OTLP] POST /v1/traces hit — url: ${c.req.url}`);
18882
19198
  const authHeader = c.req.header("authorization");
18883
19199
  if (!authHeader) return c.json({ error: "Authorization header required" }, 401);
18884
19200
  const match = authHeader.match(/^Bearer\s+(.+)$/i);
@@ -18886,11 +19202,20 @@ const app$3 = new hono.Hono().post("/v1/traces", async (c) => {
18886
19202
  const envSec = match[1].trim();
18887
19203
  c.set("envSec", envSec);
18888
19204
  let body;
19205
+ const contentType = c.req.header("content-type") ?? "";
19206
+ __llmops_core.logger.info(`[OTLP] Received request — Content-Type: ${contentType}`);
18889
19207
  try {
18890
- body = await c.req.json();
18891
- } catch {
18892
- return c.json({ error: "Invalid JSON body" }, 400);
18893
- }
19208
+ if (contentType.includes("application/x-protobuf") || contentType.includes("application/protobuf")) {
19209
+ const buffer = await c.req.arrayBuffer();
19210
+ __llmops_core.logger.info(`[OTLP] Protobuf body size: ${buffer.byteLength} bytes`);
19211
+ body = decodeOtlpProtobuf(new Uint8Array(buffer));
19212
+ } else body = await c.req.json();
19213
+ } catch (e) {
19214
+ const msg = e instanceof Error ? e.message : String(e);
19215
+ __llmops_core.logger.error(`[OTLP] Failed to parse request body: ${msg}`);
19216
+ return c.json({ error: "Invalid request body" }, 400);
19217
+ }
19218
+ __llmops_core.logger.info(`[OTLP] Parsed body — resourceSpans count: ${body.resourceSpans?.length ?? 0}`);
18894
19219
  if (!body.resourceSpans || !Array.isArray(body.resourceSpans)) return c.json({ error: "Missing resourceSpans" }, 400);
18895
19220
  const db = c.get("db");
18896
19221
  if (!db) return c.json({ error: "Database not configured" }, 503);
@@ -18903,82 +19228,90 @@ const app$3 = new hono.Hono().post("/v1/traces", async (c) => {
18903
19228
  try {
18904
19229
  for (const resourceSpan of body.resourceSpans) {
18905
19230
  const resourceAttrs = attributesToRecord(resourceSpan.resource?.attributes);
18906
- for (const scopeSpan of resourceSpan.scopeSpans) for (const otlpSpan of scopeSpan.spans) {
18907
- const allAttrs = {
18908
- ...resourceAttrs,
18909
- ...attributesToRecord(otlpSpan.attributes)
18910
- };
18911
- const typed = extractTypedFields(allAttrs);
18912
- const startTime = nanoToDate(otlpSpan.startTimeUnixNano);
18913
- const endTime = otlpSpan.endTimeUnixNano ? nanoToDate(otlpSpan.endTimeUnixNano) : null;
18914
- const durationMs = endTime ? endTime.getTime() - startTime.getTime() : null;
18915
- const spanStatus = otlpSpan.status?.code ?? 0;
18916
- const traceStatus = spanStatus === 2 ? "error" : spanStatus === 1 ? "ok" : "unset";
18917
- let cost = 0;
18918
- if (typed.provider && typed.model && (typed.promptTokens > 0 || typed.completionTokens > 0)) try {
18919
- const pricing = await pricingProvider.getModelPricing(typed.provider, typed.model);
18920
- if (pricing) cost = (0, __llmops_core.calculateCacheAwareCost)({
19231
+ __llmops_core.logger.info(`[OTLP] resourceSpan scopeSpans count: ${resourceSpan.scopeSpans?.length ?? 0}, resource attrs: ${JSON.stringify(resourceAttrs)}`);
19232
+ for (const scopeSpan of resourceSpan.scopeSpans) {
19233
+ __llmops_core.logger.info(`[OTLP] scopeSpan — scope: ${scopeSpan.scope?.name ?? "<none>"}, spans count: ${scopeSpan.spans?.length ?? 0}`);
19234
+ for (const otlpSpan of scopeSpan.spans) {
19235
+ __llmops_core.logger.info(`[OTLP] raw span — traceId: ${otlpSpan.traceId}, spanId: ${otlpSpan.spanId}, parentSpanId: ${otlpSpan.parentSpanId ?? "null"}, name: ${otlpSpan.name}, startTimeUnixNano: ${otlpSpan.startTimeUnixNano}, endTimeUnixNano: ${otlpSpan.endTimeUnixNano ?? "null"}`);
19236
+ const allAttrs = {
19237
+ ...resourceAttrs,
19238
+ ...attributesToRecord(otlpSpan.attributes)
19239
+ };
19240
+ const typed = extractTypedFields(allAttrs);
19241
+ const startTime = nanoToDate(otlpSpan.startTimeUnixNano);
19242
+ const endTime = otlpSpan.endTimeUnixNano ? nanoToDate(otlpSpan.endTimeUnixNano) : null;
19243
+ const durationMs = endTime ? endTime.getTime() - startTime.getTime() : null;
19244
+ const spanStatus = otlpSpan.status?.code ?? 0;
19245
+ const traceStatus = spanStatus === 2 ? "error" : spanStatus === 1 ? "ok" : "unset";
19246
+ let cost = 0;
19247
+ if (typed.provider && typed.model && (typed.promptTokens > 0 || typed.completionTokens > 0)) try {
19248
+ const pricing = await pricingProvider.getModelPricing(typed.provider, typed.model);
19249
+ if (pricing) cost = (0, __llmops_core.calculateCacheAwareCost)({
19250
+ promptTokens: typed.promptTokens,
19251
+ completionTokens: typed.completionTokens
19252
+ }, pricing, typed.provider).totalCost;
19253
+ } catch (e) {
19254
+ __llmops_core.logger.info(`[OTLP] Failed to calculate cost for ${typed.provider}/${typed.model}: ${e instanceof Error ? e.message : String(e)}`);
19255
+ }
19256
+ const spanData = {
19257
+ traceId: otlpSpan.traceId,
19258
+ spanId: otlpSpan.spanId,
19259
+ parentSpanId: otlpSpan.parentSpanId || null,
19260
+ name: otlpSpan.name,
19261
+ kind: otlpSpan.kind ?? 1,
19262
+ status: spanStatus,
19263
+ statusMessage: otlpSpan.status?.message ?? null,
19264
+ startTime,
19265
+ endTime,
19266
+ durationMs,
19267
+ provider: typed.provider,
19268
+ model: typed.model,
18921
19269
  promptTokens: typed.promptTokens,
18922
- completionTokens: typed.completionTokens
18923
- }, pricing, typed.provider).totalCost;
18924
- } catch (e) {
18925
- __llmops_core.logger.debug(`[OTLP] Failed to calculate cost for ${typed.provider}/${typed.model}: ${e instanceof Error ? e.message : String(e)}`);
19270
+ completionTokens: typed.completionTokens,
19271
+ totalTokens: typed.totalTokens,
19272
+ cost,
19273
+ source: "otlp",
19274
+ input: typed.input,
19275
+ output: typed.output,
19276
+ attributes: allAttrs
19277
+ };
19278
+ const spanEvents = (otlpSpan.events ?? []).map((event) => ({
19279
+ traceId: otlpSpan.traceId,
19280
+ spanId: otlpSpan.spanId,
19281
+ name: event.name,
19282
+ timestamp: nanoToDate(event.timeUnixNano),
19283
+ attributes: attributesToRecord(event.attributes)
19284
+ }));
19285
+ const traceData = {
19286
+ traceId: otlpSpan.traceId,
19287
+ name: !otlpSpan.parentSpanId ? otlpSpan.name : null,
19288
+ sessionId: allAttrs["gen_ai.conversation.id"] ?? null,
19289
+ userId: null,
19290
+ status: traceStatus,
19291
+ startTime,
19292
+ endTime,
19293
+ durationMs,
19294
+ spanCount: 1,
19295
+ totalInputTokens: typed.promptTokens,
19296
+ totalOutputTokens: typed.completionTokens,
19297
+ totalTokens: typed.totalTokens,
19298
+ totalCost: cost,
19299
+ tags: {},
19300
+ metadata: {}
19301
+ };
19302
+ __llmops_core.logger.info(`[OTLP] spanData — traceId: ${spanData.traceId}, spanId: ${spanData.spanId}, parentSpanId: ${spanData.parentSpanId ?? "null"}, name: ${spanData.name}, source: ${spanData.source}, startTime: ${spanData.startTime.toISOString()}, endTime: ${spanData.endTime?.toISOString() ?? "null"}, durationMs: ${spanData.durationMs}`);
19303
+ __llmops_core.logger.info(`[OTLP] traceData — traceId: ${traceData.traceId}, name: ${traceData.name ?? "null"}, status: ${traceData.status}, spanCount: ${traceData.spanCount}`);
19304
+ const item = {
19305
+ span: spanData,
19306
+ events: spanEvents.length > 0 ? spanEvents : void 0,
19307
+ trace: traceData
19308
+ };
19309
+ traceBatchWriter.enqueue(item);
19310
+ spanCount++;
18926
19311
  }
18927
- const spanData = {
18928
- traceId: otlpSpan.traceId,
18929
- spanId: otlpSpan.spanId,
18930
- parentSpanId: otlpSpan.parentSpanId || null,
18931
- name: otlpSpan.name,
18932
- kind: otlpSpan.kind ?? 1,
18933
- status: spanStatus,
18934
- statusMessage: otlpSpan.status?.message ?? null,
18935
- startTime,
18936
- endTime,
18937
- durationMs,
18938
- provider: typed.provider,
18939
- model: typed.model,
18940
- promptTokens: typed.promptTokens,
18941
- completionTokens: typed.completionTokens,
18942
- totalTokens: typed.totalTokens,
18943
- cost,
18944
- source: "otlp",
18945
- input: typed.input,
18946
- output: typed.output,
18947
- attributes: allAttrs
18948
- };
18949
- const spanEvents = (otlpSpan.events ?? []).map((event) => ({
18950
- traceId: otlpSpan.traceId,
18951
- spanId: otlpSpan.spanId,
18952
- name: event.name,
18953
- timestamp: nanoToDate(event.timeUnixNano),
18954
- attributes: attributesToRecord(event.attributes)
18955
- }));
18956
- const traceData = {
18957
- traceId: otlpSpan.traceId,
18958
- name: !otlpSpan.parentSpanId ? otlpSpan.name : null,
18959
- sessionId: allAttrs["gen_ai.conversation.id"] ?? null,
18960
- userId: null,
18961
- status: traceStatus,
18962
- startTime,
18963
- endTime,
18964
- durationMs,
18965
- spanCount: 1,
18966
- totalInputTokens: typed.promptTokens,
18967
- totalOutputTokens: typed.completionTokens,
18968
- totalTokens: typed.totalTokens,
18969
- totalCost: cost,
18970
- tags: {},
18971
- metadata: {}
18972
- };
18973
- const item = {
18974
- span: spanData,
18975
- events: spanEvents.length > 0 ? spanEvents : void 0,
18976
- trace: traceData
18977
- };
18978
- traceBatchWriter.enqueue(item);
18979
- spanCount++;
18980
19312
  }
18981
19313
  }
19314
+ __llmops_core.logger.info(`[OTLP] Enqueued ${spanCount} spans total`);
18982
19315
  return c.json({ partialSuccess: {} });
18983
19316
  } catch (error$47) {
18984
19317
  const msg = error$47 instanceof Error ? error$47.message : String(error$47);
package/dist/index.mjs CHANGED
@@ -14,6 +14,7 @@ import { prettyJSON } from "hono/pretty-json";
14
14
  import { cors } from "hono/cors";
15
15
  import { randomBytes, randomUUID } from "node:crypto";
16
16
  import gateway from "@llmops/gateway";
17
+ import protobuf from "protobufjs";
17
18
  import { env } from "node:process";
18
19
  import { createDatabaseFromConnection, detectDatabaseType } from "@llmops/core/db";
19
20
  import { betterAuth } from "better-auth";
@@ -18259,6 +18260,7 @@ function createTraceBatchWriter(deps, config$1 = {}) {
18259
18260
  const batch = queue;
18260
18261
  queue = [];
18261
18262
  try {
18263
+ logger.info(`[TraceBatchWriter] Flushing ${batch.length} items`);
18262
18264
  log(`[TraceBatchWriter] Flushing ${batch.length} items`);
18263
18265
  const traceMap = /* @__PURE__ */ new Map();
18264
18266
  for (const item of batch) {
@@ -18292,6 +18294,7 @@ function createTraceBatchWriter(deps, config$1 = {}) {
18292
18294
  if (allSpans.length > 0) await deps.batchInsertSpans(allSpans);
18293
18295
  const allEvents = batch.flatMap((item) => item.events ?? []);
18294
18296
  if (allEvents.length > 0) await deps.batchInsertSpanEvents(allEvents);
18297
+ logger.info(`[TraceBatchWriter] Flushed ${traceMap.size} traces, ${allSpans.length} spans, ${allEvents.length} events`);
18295
18298
  log(`[TraceBatchWriter] Flushed ${traceMap.size} traces, ${allSpans.length} spans, ${allEvents.length} events`);
18296
18299
  } catch (error$47) {
18297
18300
  const errorMsg = error$47 instanceof Error ? error$47.message : String(error$47);
@@ -18780,6 +18783,317 @@ app$4.use("*", prettyJSON()).get("/health", async (c) => {
18780
18783
  });
18781
18784
  var genai_default = app$4;
18782
18785
 
18786
+ //#endregion
18787
+ //#region src/server/handlers/otlp/decode.ts
18788
+ /**
18789
+ * OTLP Protobuf Decoder
18790
+ *
18791
+ * Decodes binary protobuf ExportTraceServiceRequest into the same JSON
18792
+ * structure the OTLP handler already expects. Uses an embedded protobufjs
18793
+ * JSON descriptor — no .proto files needed at runtime.
18794
+ */
18795
+ const protoDescriptor = { nested: {
18796
+ ExportTraceServiceRequest: { fields: { resourceSpans: {
18797
+ rule: "repeated",
18798
+ type: "ResourceSpans",
18799
+ id: 1
18800
+ } } },
18801
+ ResourceSpans: { fields: {
18802
+ resource: {
18803
+ type: "Resource",
18804
+ id: 1
18805
+ },
18806
+ scopeSpans: {
18807
+ rule: "repeated",
18808
+ type: "ScopeSpans",
18809
+ id: 2
18810
+ },
18811
+ instrumentationLibrarySpans: {
18812
+ rule: "repeated",
18813
+ type: "ScopeSpans",
18814
+ id: 1e3
18815
+ }
18816
+ } },
18817
+ Resource: { fields: { attributes: {
18818
+ rule: "repeated",
18819
+ type: "KeyValue",
18820
+ id: 1
18821
+ } } },
18822
+ ScopeSpans: { fields: {
18823
+ scope: {
18824
+ type: "InstrumentationScope",
18825
+ id: 1
18826
+ },
18827
+ spans: {
18828
+ rule: "repeated",
18829
+ type: "Span",
18830
+ id: 2
18831
+ }
18832
+ } },
18833
+ InstrumentationScope: { fields: {
18834
+ name: {
18835
+ type: "string",
18836
+ id: 1
18837
+ },
18838
+ version: {
18839
+ type: "string",
18840
+ id: 2
18841
+ }
18842
+ } },
18843
+ Span: { fields: {
18844
+ traceId: {
18845
+ type: "bytes",
18846
+ id: 1
18847
+ },
18848
+ spanId: {
18849
+ type: "bytes",
18850
+ id: 2
18851
+ },
18852
+ traceState: {
18853
+ type: "string",
18854
+ id: 3
18855
+ },
18856
+ parentSpanId: {
18857
+ type: "bytes",
18858
+ id: 4
18859
+ },
18860
+ name: {
18861
+ type: "string",
18862
+ id: 5
18863
+ },
18864
+ kind: {
18865
+ type: "SpanKind",
18866
+ id: 6
18867
+ },
18868
+ startTimeUnixNano: {
18869
+ type: "fixed64",
18870
+ id: 7
18871
+ },
18872
+ endTimeUnixNano: {
18873
+ type: "fixed64",
18874
+ id: 8
18875
+ },
18876
+ attributes: {
18877
+ rule: "repeated",
18878
+ type: "KeyValue",
18879
+ id: 9
18880
+ },
18881
+ events: {
18882
+ rule: "repeated",
18883
+ type: "SpanEvent",
18884
+ id: 10
18885
+ },
18886
+ status: {
18887
+ type: "Status",
18888
+ id: 15
18889
+ }
18890
+ } },
18891
+ SpanKind: { values: {
18892
+ SPAN_KIND_UNSPECIFIED: 0,
18893
+ SPAN_KIND_INTERNAL: 1,
18894
+ SPAN_KIND_SERVER: 2,
18895
+ SPAN_KIND_CLIENT: 3,
18896
+ SPAN_KIND_PRODUCER: 4,
18897
+ SPAN_KIND_CONSUMER: 5
18898
+ } },
18899
+ SpanEvent: { fields: {
18900
+ timeUnixNano: {
18901
+ type: "fixed64",
18902
+ id: 1
18903
+ },
18904
+ name: {
18905
+ type: "string",
18906
+ id: 2
18907
+ },
18908
+ attributes: {
18909
+ rule: "repeated",
18910
+ type: "KeyValue",
18911
+ id: 3
18912
+ }
18913
+ } },
18914
+ Status: { fields: {
18915
+ message: {
18916
+ type: "string",
18917
+ id: 2
18918
+ },
18919
+ code: {
18920
+ type: "StatusCode",
18921
+ id: 3
18922
+ }
18923
+ } },
18924
+ StatusCode: { values: {
18925
+ STATUS_CODE_UNSET: 0,
18926
+ STATUS_CODE_OK: 1,
18927
+ STATUS_CODE_ERROR: 2
18928
+ } },
18929
+ KeyValue: { fields: {
18930
+ key: {
18931
+ type: "string",
18932
+ id: 1
18933
+ },
18934
+ value: {
18935
+ type: "AnyValue",
18936
+ id: 2
18937
+ }
18938
+ } },
18939
+ AnyValue: {
18940
+ fields: {
18941
+ stringValue: {
18942
+ type: "string",
18943
+ id: 1
18944
+ },
18945
+ boolValue: {
18946
+ type: "bool",
18947
+ id: 2
18948
+ },
18949
+ intValue: {
18950
+ type: "int64",
18951
+ id: 3
18952
+ },
18953
+ doubleValue: {
18954
+ type: "double",
18955
+ id: 4
18956
+ },
18957
+ arrayValue: {
18958
+ type: "ArrayValue",
18959
+ id: 5
18960
+ },
18961
+ kvlistValue: {
18962
+ type: "KeyValueList",
18963
+ id: 6
18964
+ },
18965
+ bytesValue: {
18966
+ type: "bytes",
18967
+ id: 7
18968
+ }
18969
+ },
18970
+ oneofs: { value: { oneof: [
18971
+ "stringValue",
18972
+ "boolValue",
18973
+ "intValue",
18974
+ "doubleValue",
18975
+ "arrayValue",
18976
+ "kvlistValue",
18977
+ "bytesValue"
18978
+ ] } }
18979
+ },
18980
+ ArrayValue: { fields: { values: {
18981
+ rule: "repeated",
18982
+ type: "AnyValue",
18983
+ id: 1
18984
+ } } },
18985
+ KeyValueList: { fields: { values: {
18986
+ rule: "repeated",
18987
+ type: "KeyValue",
18988
+ id: 1
18989
+ } } }
18990
+ } };
18991
+ let _root = null;
18992
+ let _RequestType = null;
18993
+ function getRequestType() {
18994
+ if (!_RequestType) {
18995
+ _root = protobuf.Root.fromJSON(protoDescriptor);
18996
+ _RequestType = _root.lookupType("ExportTraceServiceRequest");
18997
+ }
18998
+ return _RequestType;
18999
+ }
19000
+ function bytesToHex(bytes) {
19001
+ return Buffer.from(bytes).toString("hex");
19002
+ }
19003
+ function isZeroBytes(bytes) {
19004
+ for (let i = 0; i < bytes.length; i++) if (bytes[i] !== 0) return false;
19005
+ return true;
19006
+ }
19007
+ /**
19008
+ * Convert a protobufjs Long / number / string to a nanosecond string.
19009
+ * protobufjs decodes fixed64 as Long when longs are not forced to number.
19010
+ */
19011
+ function toNanoString(val) {
19012
+ if (val == null) return "0";
19013
+ if (typeof val === "object" && val !== null && "toString" in val) return val.toString();
19014
+ return String(val);
19015
+ }
19016
+ /**
19017
+ * Normalise an AnyValue from decoded protobuf into the OTLP JSON shape.
19018
+ */
19019
+ function normaliseAnyValue(av) {
19020
+ const out = {};
19021
+ if (av.stringValue !== void 0 && av.stringValue !== "") out.stringValue = av.stringValue;
19022
+ if (av.boolValue !== void 0 && av.boolValue !== false) out.boolValue = av.boolValue;
19023
+ if (av.intValue !== void 0) {
19024
+ const v = av.intValue;
19025
+ out.intValue = typeof v === "object" && v !== null && "toNumber" in v ? v.toNumber() : Number(v);
19026
+ }
19027
+ if (av.doubleValue !== void 0 && av.doubleValue !== 0) out.doubleValue = av.doubleValue;
19028
+ if (av.arrayValue !== void 0 && av.arrayValue !== null) out.arrayValue = { values: (av.arrayValue.values ?? []).map((v) => normaliseAnyValue(v)) };
19029
+ if (av.kvlistValue !== void 0 && av.kvlistValue !== null) out.kvlistValue = { values: (av.kvlistValue.values ?? []).map((kv) => normaliseKeyValue(kv)) };
19030
+ if (av.bytesValue !== void 0) out.bytesValue = bytesToHex(av.bytesValue);
19031
+ return out;
19032
+ }
19033
+ function normaliseKeyValue(kv) {
19034
+ return {
19035
+ key: kv.key,
19036
+ value: normaliseAnyValue(kv.value ?? {})
19037
+ };
19038
+ }
19039
+ /**
19040
+ * Decode an OTLP protobuf-encoded ExportTraceServiceRequest binary into
19041
+ * the same JSON structure the handler expects.
19042
+ *
19043
+ * - bytes fields (trace_id, span_id, parent_span_id) → hex strings
19044
+ * - fixed64 nano timestamps → string (for nanoToDate())
19045
+ * - Long intValue → number
19046
+ * - deprecated instrumentationLibrarySpans merged into scopeSpans
19047
+ */
19048
+ function decodeOtlpProtobuf(buffer) {
19049
+ return { resourceSpans: (getRequestType().decode(buffer).resourceSpans ?? []).map((rs) => {
19050
+ const rawResource = rs.resource;
19051
+ return {
19052
+ resource: rawResource ? { attributes: (rawResource.attributes ?? []).map(normaliseKeyValue) } : void 0,
19053
+ scopeSpans: [...rs.scopeSpans ?? [], ...rs.instrumentationLibrarySpans ?? []].map((ss) => {
19054
+ const rawScope = ss.scope;
19055
+ const spans = (ss.spans ?? []).map((s) => {
19056
+ const traceIdBytes = s.traceId;
19057
+ const spanIdBytes = s.spanId;
19058
+ const parentSpanIdBytes = s.parentSpanId;
19059
+ const traceId = traceIdBytes ? bytesToHex(traceIdBytes) : "";
19060
+ const spanId = spanIdBytes ? bytesToHex(spanIdBytes) : "";
19061
+ const parentSpanId = parentSpanIdBytes && parentSpanIdBytes.length > 0 && !isZeroBytes(parentSpanIdBytes) ? bytesToHex(parentSpanIdBytes) : void 0;
19062
+ const attributes = (s.attributes ?? []).map(normaliseKeyValue);
19063
+ const events = (s.events ?? []).map((e) => ({
19064
+ name: e.name ?? "",
19065
+ timeUnixNano: toNanoString(e.timeUnixNano),
19066
+ attributes: (e.attributes ?? []).map(normaliseKeyValue)
19067
+ }));
19068
+ const rawStatus = s.status;
19069
+ return {
19070
+ traceId,
19071
+ spanId,
19072
+ parentSpanId,
19073
+ name: s.name ?? "",
19074
+ kind: s.kind,
19075
+ startTimeUnixNano: toNanoString(s.startTimeUnixNano),
19076
+ endTimeUnixNano: s.endTimeUnixNano ? toNanoString(s.endTimeUnixNano) : void 0,
19077
+ attributes,
19078
+ events: events.length > 0 ? events : void 0,
19079
+ status: rawStatus ? {
19080
+ code: rawStatus.code,
19081
+ message: rawStatus.message
19082
+ } : void 0
19083
+ };
19084
+ });
19085
+ return {
19086
+ scope: rawScope ? {
19087
+ name: rawScope.name,
19088
+ version: rawScope.version
19089
+ } : void 0,
19090
+ spans
19091
+ };
19092
+ })
19093
+ };
19094
+ }) };
19095
+ }
19096
+
18783
19097
  //#endregion
18784
19098
  //#region src/server/handlers/otlp/index.ts
18785
19099
  /**
@@ -18848,9 +19162,10 @@ function extractTypedFields(attrs) {
18848
19162
  const pricingProvider = getDefaultPricingProvider();
18849
19163
  /**
18850
19164
  * OTLP ingestion endpoint
18851
- * Accepts OTLP JSON (ExportTraceServiceRequest) format
19165
+ * Accepts OTLP JSON and Protobuf (ExportTraceServiceRequest) formats
18852
19166
  */
18853
19167
  const app$3 = new Hono().post("/v1/traces", async (c) => {
19168
+ logger.info(`[OTLP] POST /v1/traces hit — url: ${c.req.url}`);
18854
19169
  const authHeader = c.req.header("authorization");
18855
19170
  if (!authHeader) return c.json({ error: "Authorization header required" }, 401);
18856
19171
  const match = authHeader.match(/^Bearer\s+(.+)$/i);
@@ -18858,11 +19173,20 @@ const app$3 = new Hono().post("/v1/traces", async (c) => {
18858
19173
  const envSec = match[1].trim();
18859
19174
  c.set("envSec", envSec);
18860
19175
  let body;
19176
+ const contentType = c.req.header("content-type") ?? "";
19177
+ logger.info(`[OTLP] Received request — Content-Type: ${contentType}`);
18861
19178
  try {
18862
- body = await c.req.json();
18863
- } catch {
18864
- return c.json({ error: "Invalid JSON body" }, 400);
18865
- }
19179
+ if (contentType.includes("application/x-protobuf") || contentType.includes("application/protobuf")) {
19180
+ const buffer = await c.req.arrayBuffer();
19181
+ logger.info(`[OTLP] Protobuf body size: ${buffer.byteLength} bytes`);
19182
+ body = decodeOtlpProtobuf(new Uint8Array(buffer));
19183
+ } else body = await c.req.json();
19184
+ } catch (e) {
19185
+ const msg = e instanceof Error ? e.message : String(e);
19186
+ logger.error(`[OTLP] Failed to parse request body: ${msg}`);
19187
+ return c.json({ error: "Invalid request body" }, 400);
19188
+ }
19189
+ logger.info(`[OTLP] Parsed body — resourceSpans count: ${body.resourceSpans?.length ?? 0}`);
18866
19190
  if (!body.resourceSpans || !Array.isArray(body.resourceSpans)) return c.json({ error: "Missing resourceSpans" }, 400);
18867
19191
  const db = c.get("db");
18868
19192
  if (!db) return c.json({ error: "Database not configured" }, 503);
@@ -18875,82 +19199,90 @@ const app$3 = new Hono().post("/v1/traces", async (c) => {
18875
19199
  try {
18876
19200
  for (const resourceSpan of body.resourceSpans) {
18877
19201
  const resourceAttrs = attributesToRecord(resourceSpan.resource?.attributes);
18878
- for (const scopeSpan of resourceSpan.scopeSpans) for (const otlpSpan of scopeSpan.spans) {
18879
- const allAttrs = {
18880
- ...resourceAttrs,
18881
- ...attributesToRecord(otlpSpan.attributes)
18882
- };
18883
- const typed = extractTypedFields(allAttrs);
18884
- const startTime = nanoToDate(otlpSpan.startTimeUnixNano);
18885
- const endTime = otlpSpan.endTimeUnixNano ? nanoToDate(otlpSpan.endTimeUnixNano) : null;
18886
- const durationMs = endTime ? endTime.getTime() - startTime.getTime() : null;
18887
- const spanStatus = otlpSpan.status?.code ?? 0;
18888
- const traceStatus = spanStatus === 2 ? "error" : spanStatus === 1 ? "ok" : "unset";
18889
- let cost = 0;
18890
- if (typed.provider && typed.model && (typed.promptTokens > 0 || typed.completionTokens > 0)) try {
18891
- const pricing = await pricingProvider.getModelPricing(typed.provider, typed.model);
18892
- if (pricing) cost = calculateCacheAwareCost({
19202
+ logger.info(`[OTLP] resourceSpan scopeSpans count: ${resourceSpan.scopeSpans?.length ?? 0}, resource attrs: ${JSON.stringify(resourceAttrs)}`);
19203
+ for (const scopeSpan of resourceSpan.scopeSpans) {
19204
+ logger.info(`[OTLP] scopeSpan — scope: ${scopeSpan.scope?.name ?? "<none>"}, spans count: ${scopeSpan.spans?.length ?? 0}`);
19205
+ for (const otlpSpan of scopeSpan.spans) {
19206
+ logger.info(`[OTLP] raw span — traceId: ${otlpSpan.traceId}, spanId: ${otlpSpan.spanId}, parentSpanId: ${otlpSpan.parentSpanId ?? "null"}, name: ${otlpSpan.name}, startTimeUnixNano: ${otlpSpan.startTimeUnixNano}, endTimeUnixNano: ${otlpSpan.endTimeUnixNano ?? "null"}`);
19207
+ const allAttrs = {
19208
+ ...resourceAttrs,
19209
+ ...attributesToRecord(otlpSpan.attributes)
19210
+ };
19211
+ const typed = extractTypedFields(allAttrs);
19212
+ const startTime = nanoToDate(otlpSpan.startTimeUnixNano);
19213
+ const endTime = otlpSpan.endTimeUnixNano ? nanoToDate(otlpSpan.endTimeUnixNano) : null;
19214
+ const durationMs = endTime ? endTime.getTime() - startTime.getTime() : null;
19215
+ const spanStatus = otlpSpan.status?.code ?? 0;
19216
+ const traceStatus = spanStatus === 2 ? "error" : spanStatus === 1 ? "ok" : "unset";
19217
+ let cost = 0;
19218
+ if (typed.provider && typed.model && (typed.promptTokens > 0 || typed.completionTokens > 0)) try {
19219
+ const pricing = await pricingProvider.getModelPricing(typed.provider, typed.model);
19220
+ if (pricing) cost = calculateCacheAwareCost({
19221
+ promptTokens: typed.promptTokens,
19222
+ completionTokens: typed.completionTokens
19223
+ }, pricing, typed.provider).totalCost;
19224
+ } catch (e) {
19225
+ logger.info(`[OTLP] Failed to calculate cost for ${typed.provider}/${typed.model}: ${e instanceof Error ? e.message : String(e)}`);
19226
+ }
19227
+ const spanData = {
19228
+ traceId: otlpSpan.traceId,
19229
+ spanId: otlpSpan.spanId,
19230
+ parentSpanId: otlpSpan.parentSpanId || null,
19231
+ name: otlpSpan.name,
19232
+ kind: otlpSpan.kind ?? 1,
19233
+ status: spanStatus,
19234
+ statusMessage: otlpSpan.status?.message ?? null,
19235
+ startTime,
19236
+ endTime,
19237
+ durationMs,
19238
+ provider: typed.provider,
19239
+ model: typed.model,
18893
19240
  promptTokens: typed.promptTokens,
18894
- completionTokens: typed.completionTokens
18895
- }, pricing, typed.provider).totalCost;
18896
- } catch (e) {
18897
- logger.debug(`[OTLP] Failed to calculate cost for ${typed.provider}/${typed.model}: ${e instanceof Error ? e.message : String(e)}`);
19241
+ completionTokens: typed.completionTokens,
19242
+ totalTokens: typed.totalTokens,
19243
+ cost,
19244
+ source: "otlp",
19245
+ input: typed.input,
19246
+ output: typed.output,
19247
+ attributes: allAttrs
19248
+ };
19249
+ const spanEvents = (otlpSpan.events ?? []).map((event) => ({
19250
+ traceId: otlpSpan.traceId,
19251
+ spanId: otlpSpan.spanId,
19252
+ name: event.name,
19253
+ timestamp: nanoToDate(event.timeUnixNano),
19254
+ attributes: attributesToRecord(event.attributes)
19255
+ }));
19256
+ const traceData = {
19257
+ traceId: otlpSpan.traceId,
19258
+ name: !otlpSpan.parentSpanId ? otlpSpan.name : null,
19259
+ sessionId: allAttrs["gen_ai.conversation.id"] ?? null,
19260
+ userId: null,
19261
+ status: traceStatus,
19262
+ startTime,
19263
+ endTime,
19264
+ durationMs,
19265
+ spanCount: 1,
19266
+ totalInputTokens: typed.promptTokens,
19267
+ totalOutputTokens: typed.completionTokens,
19268
+ totalTokens: typed.totalTokens,
19269
+ totalCost: cost,
19270
+ tags: {},
19271
+ metadata: {}
19272
+ };
19273
+ logger.info(`[OTLP] spanData — traceId: ${spanData.traceId}, spanId: ${spanData.spanId}, parentSpanId: ${spanData.parentSpanId ?? "null"}, name: ${spanData.name}, source: ${spanData.source}, startTime: ${spanData.startTime.toISOString()}, endTime: ${spanData.endTime?.toISOString() ?? "null"}, durationMs: ${spanData.durationMs}`);
19274
+ logger.info(`[OTLP] traceData — traceId: ${traceData.traceId}, name: ${traceData.name ?? "null"}, status: ${traceData.status}, spanCount: ${traceData.spanCount}`);
19275
+ const item = {
19276
+ span: spanData,
19277
+ events: spanEvents.length > 0 ? spanEvents : void 0,
19278
+ trace: traceData
19279
+ };
19280
+ traceBatchWriter.enqueue(item);
19281
+ spanCount++;
18898
19282
  }
18899
- const spanData = {
18900
- traceId: otlpSpan.traceId,
18901
- spanId: otlpSpan.spanId,
18902
- parentSpanId: otlpSpan.parentSpanId || null,
18903
- name: otlpSpan.name,
18904
- kind: otlpSpan.kind ?? 1,
18905
- status: spanStatus,
18906
- statusMessage: otlpSpan.status?.message ?? null,
18907
- startTime,
18908
- endTime,
18909
- durationMs,
18910
- provider: typed.provider,
18911
- model: typed.model,
18912
- promptTokens: typed.promptTokens,
18913
- completionTokens: typed.completionTokens,
18914
- totalTokens: typed.totalTokens,
18915
- cost,
18916
- source: "otlp",
18917
- input: typed.input,
18918
- output: typed.output,
18919
- attributes: allAttrs
18920
- };
18921
- const spanEvents = (otlpSpan.events ?? []).map((event) => ({
18922
- traceId: otlpSpan.traceId,
18923
- spanId: otlpSpan.spanId,
18924
- name: event.name,
18925
- timestamp: nanoToDate(event.timeUnixNano),
18926
- attributes: attributesToRecord(event.attributes)
18927
- }));
18928
- const traceData = {
18929
- traceId: otlpSpan.traceId,
18930
- name: !otlpSpan.parentSpanId ? otlpSpan.name : null,
18931
- sessionId: allAttrs["gen_ai.conversation.id"] ?? null,
18932
- userId: null,
18933
- status: traceStatus,
18934
- startTime,
18935
- endTime,
18936
- durationMs,
18937
- spanCount: 1,
18938
- totalInputTokens: typed.promptTokens,
18939
- totalOutputTokens: typed.completionTokens,
18940
- totalTokens: typed.totalTokens,
18941
- totalCost: cost,
18942
- tags: {},
18943
- metadata: {}
18944
- };
18945
- const item = {
18946
- span: spanData,
18947
- events: spanEvents.length > 0 ? spanEvents : void 0,
18948
- trace: traceData
18949
- };
18950
- traceBatchWriter.enqueue(item);
18951
- spanCount++;
18952
19283
  }
18953
19284
  }
19285
+ logger.info(`[OTLP] Enqueued ${spanCount} spans total`);
18954
19286
  return c.json({ partialSuccess: {} });
18955
19287
  } catch (error$47) {
18956
19288
  const msg = error$47 instanceof Error ? error$47.message : String(error$47);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llmops/app",
3
- "version": "0.6.3",
3
+ "version": "0.6.6",
4
4
  "description": "LLMOps application with server and client",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -72,12 +72,13 @@
72
72
  "motion": "^12.23.25",
73
73
  "nunjucks": "^3.2.4",
74
74
  "openai": "^6.10.0",
75
+ "protobufjs": "^8.0.0",
75
76
  "react-aria-components": "^1.13.0",
76
77
  "react-hook-form": "^7.68.0",
77
78
  "recharts": "^3.6.0",
78
79
  "uuid": "^13.0.0",
79
- "@llmops/core": "^0.6.3",
80
- "@llmops/gateway": "^0.6.3"
80
+ "@llmops/core": "^0.6.6",
81
+ "@llmops/gateway": "^0.6.6"
81
82
  },
82
83
  "peerDependencies": {
83
84
  "react": "^19.2.1",