Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions gateway/test/otel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
interface OtlpAttribute {
key: string
value: OtlpValue
}

interface OtlpValue {
stringValue?: string
intValue?: number
doubleValue?: number
boolValue?: boolean
arrayValue?: { values?: OtlpValue[] }
kvlistValue?: { values?: { key: string; value: OtlpValue }[] }
}

/**
* Deserializes OTLP trace data from a JSON string and transforms it into a cleaner JSON structure.
* This extracts the key information from the OTLP format (resource spans, scope spans, etc.)
* and returns a more readable representation suitable for test snapshots.
*/
export function deserializeRequest(data: string) {
const otlpData = JSON.parse(data)

// Transform OTLP format into cleaner JSON
const spans = []

for (const resourceSpan of otlpData.resourceSpans || []) {
const resource = resourceSpan.resource?.attributes || []

for (const scopeSpan of resourceSpan.scopeSpans || []) {
const scope = scopeSpan.scope?.name

for (const span of scopeSpan.spans || []) {
// Convert attributes array to object
const attributes: Record<string, unknown> = {}
for (const attr of span.attributes || []) {
attributes[attr.key] = extractOtlpValue(attr.value)
}

spans.push({
name: span.name,
parentSpanId: span.parentSpanId,
kind: span.kind,
attributes,
status: span.status,
events: span.events,
links: span.links,
resource: Object.fromEntries(
resource.map((attr: OtlpAttribute) => [
attr.key,
attr.value.stringValue ??
attr.value.intValue ??
attr.value.doubleValue ??
attr.value.boolValue ??
attr.value,
]),
),
scope,
})
}
}
}

return spans
}

/**
* Recursively extracts the actual value from an OTLP AnyValue structure.
* Handles all OTLP value types including nested structures.
*/
function extractOtlpValue(value: OtlpValue): unknown {
if (value.stringValue !== undefined) return value.stringValue
if (value.intValue !== undefined) return value.intValue
if (value.doubleValue !== undefined) return value.doubleValue
if (value.boolValue !== undefined) return value.boolValue

if (value.arrayValue) {
// For arrays, just extract the values directly without wrapping
return value.arrayValue.values?.map(extractOtlpValue) || []
}

if (value.kvlistValue) {
// For key-value lists, extract as an object
const obj: Record<string, unknown> = {}
for (const kv of value.kvlistValue.values || []) {
obj[kv.key] = extractOtlpValue(kv.value)
}
return obj
}

// For empty objects or unknown types, return the raw value
return value
}
7 changes: 4 additions & 3 deletions gateway/test/providers/anthropic.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Anthropic from '@anthropic-ai/sdk'
import { describe, expect } from 'vitest'
import { deserializeRequest } from '../otel'
import { test } from '../setup'

describe('anthropic', () => {
Expand All @@ -21,7 +22,7 @@ describe('anthropic', () => {
})
expect(completion).toMatchSnapshot('llm')
expect(otelBatch, 'otelBatch length not 1').toHaveLength(1)
expect(JSON.parse(otelBatch[0]!).resourceSpans?.[0].scopeSpans?.[0].spans?.[0]?.attributes).toMatchSnapshot('span')
expect(deserializeRequest(otelBatch[0]!)).toMatchSnapshot('span')
})

test('should call anthropic via gateway with builtin tools', async ({ gateway }) => {
Expand All @@ -40,7 +41,7 @@ describe('anthropic', () => {
})
expect(response).toMatchSnapshot('llm')
expect(otelBatch, 'otelBatch length not 1').toHaveLength(1)
expect(JSON.parse(otelBatch[0]!).resourceSpans?.[0].scopeSpans?.[0].spans?.[0]?.attributes).toMatchSnapshot('span')
expect(deserializeRequest(otelBatch[0]!)).toMatchSnapshot('span')
})

test('should call anthropic via gateway with stream', async ({ gateway }) => {
Expand All @@ -61,6 +62,6 @@ describe('anthropic', () => {

expect(chunks).toMatchSnapshot('chunks')
expect(otelBatch, 'otelBatch length not 1').toHaveLength(1)
expect(JSON.parse(otelBatch[0]!).resourceSpans?.[0].scopeSpans?.[0].spans?.[0]?.attributes).toMatchSnapshot('span')
expect(deserializeRequest(otelBatch[0]!)).toMatchSnapshot('span')
})
})
Loading