Files
wei-shaw--claude-relay-service/tests/requestDetailHelper.test.js
T
2026-07-13 12:48:39 +08:00

321 lines
9.1 KiB
JavaScript

const {
sanitizeRequestBodySnapshot,
extractRequestReasoningInfo,
resolveRequestDetailReasoning,
createRequestDetailMeta,
finalizeRequestDetailMeta,
extractOpenAICacheReadTokens,
isOpenAIRelatedEndpoint,
getRequestDetailCacheMetrics,
calculateCacheHitRate
} = require('../src/utils/requestDetailHelper')
describe('requestDetailHelper', () => {
afterEach(() => {
jest.useRealTimers()
})
test('sanitizeRequestBodySnapshot redacts secrets and truncates long text', () => {
const snapshot = sanitizeRequestBodySnapshot({
apiKey: 'super-secret-api-key',
messages: [
{
role: 'user',
content: 'x'.repeat(400)
}
]
})
expect(snapshot.apiKey).toContain('***')
expect(snapshot.messages[0].content).toBe(`${'x'.repeat(80)}...[320 chars]`)
})
test('sanitizeRequestBodySnapshot keeps all object keys while truncating long values', () => {
const payload = Object.fromEntries(
Array.from({ length: 50 }, (_, index) => [
`key_${index}`,
`value-${index}-${'x'.repeat(100)}`
])
)
const snapshot = sanitizeRequestBodySnapshot(payload)
expect(Object.keys(snapshot)).toHaveLength(50)
expect(snapshot.__truncatedKeys).toBeUndefined()
expect(snapshot.key_0).toBe(`value-0-${'x'.repeat(72)}...[28 chars]`)
})
test('sanitizeRequestBodySnapshot still wraps oversized payloads in preview metadata', () => {
const snapshot = sanitizeRequestBodySnapshot(
Object.fromEntries(
Array.from({ length: 220 }, (_, index) => [`key_${index}`, `${index}-${'x'.repeat(120)}`])
)
)
expect(snapshot.summary).toBe('request body snapshot truncated')
expect(snapshot.maxChars).toBe(12000)
expect(typeof snapshot.preview).toBe('string')
expect(snapshot.preview).toContain('...[')
})
test('sanitizeRequestBodySnapshot omits encrypted_content values', () => {
const snapshot = sanitizeRequestBodySnapshot({
reasoning: {
encrypted_content: 'x'.repeat(512)
}
})
expect(snapshot.reasoning.encrypted_content).toBe('...[512 chars]')
})
test('sanitizeRequestBodySnapshot keeps only tool type and name', () => {
const snapshot = sanitizeRequestBodySnapshot({
tools: [
{
type: 'function',
function: {
name: 'lookup_weather',
description: 'Weather lookup',
parameters: {
type: 'object',
properties: {
city: {
type: 'string'
}
}
}
}
},
{
name: 'claude_tool',
description: 'Anthropic style tool',
input_schema: {
type: 'object'
}
}
]
})
expect(snapshot.tools).toEqual([
{ type: 'function', name: 'lookup_weather' },
{ name: 'claude_tool' }
])
})
test('extractRequestReasoningInfo supports openai, anthropic, and gemini payloads', () => {
expect(
extractRequestReasoningInfo({
reasoning: {
effort: 'xhigh'
}
})
).toEqual({
reasoningDisplay: 'xhigh',
reasoningSource: 'reasoning.effort'
})
expect(
extractRequestReasoningInfo({
output_config: {
effort: 'medium'
}
})
).toEqual({
reasoningDisplay: 'medium',
reasoningSource: 'output_config.effort'
})
expect(
extractRequestReasoningInfo({
generationConfig: {
thinkingConfig: {
thinkingBudget: -1
}
}
})
).toEqual({
reasoningDisplay: 'dynamic',
reasoningSource: 'generationConfig.thinkingConfig.thinkingBudget'
})
})
test('resolveRequestDetailReasoning falls back to stored preview text when needed', () => {
expect(
resolveRequestDetailReasoning({
requestBodySnapshot: {
preview:
'{"model":"gpt-5.4-mini","reasoning":{"effort":"high","summary":"auto"}}...[25 chars]'
}
})
).toEqual({
reasoningDisplay: 'high',
reasoningSource: 'reasoning.effort'
})
expect(
resolveRequestDetailReasoning({
requestBodySnapshot: {
preview:
'{"model":"claude-opus-4-6","thinking":{"type":"enabled","budget_tokens":4096}...[60 chars]'
}
})
).toEqual({
reasoningDisplay: 'enabled / budget:4096',
reasoningSource: 'thinking.type,thinking.budget_tokens'
})
})
test('createRequestDetailMeta derives endpoint and duration from request', () => {
const now = Date.now()
const req = {
requestId: 'req_123',
originalUrl: '/v1/messages?stream=true',
method: 'POST',
requestStartedAt: now - 250,
body: { model: 'claude-sonnet-4-6', stream: true },
res: { statusCode: 201 }
}
const meta = createRequestDetailMeta(req)
expect(meta.requestId).toBe('req_123')
expect(meta.endpoint).toBe('/v1/messages')
expect(meta.method).toBe('POST')
expect(meta.stream).toBe(true)
expect(meta.statusCode).toBe(201)
expect(meta.durationMs).toBeGreaterThanOrEqual(200)
expect(meta.requestBody).toEqual(req.body)
})
test('finalizeRequestDetailMeta refreshes duration from requestStartedAt', () => {
jest.useFakeTimers().setSystemTime(Date.parse('2026-04-09T05:00:00.500Z'))
const meta = finalizeRequestDetailMeta({
requestId: 'req_123',
requestStartedAt: '2026-04-09T05:00:00.000Z',
durationMs: 25
})
expect(meta.durationMs).toBe(500)
})
test('identifies openai-style request detail endpoints', () => {
expect(isOpenAIRelatedEndpoint('/openai/v1/responses')).toBe(true)
expect(isOpenAIRelatedEndpoint('/openai/responses')).toBe(true)
expect(isOpenAIRelatedEndpoint('/azure/chat/completions')).toBe(true)
expect(isOpenAIRelatedEndpoint('/droid/openai/v1/responses')).toBe(true)
expect(isOpenAIRelatedEndpoint('/openai/claude/v1/messages')).toBe(false)
expect(isOpenAIRelatedEndpoint('/v1/messages')).toBe(false)
})
test('extractOpenAICacheReadTokens prefers input_tokens_details.cached_tokens', () => {
expect(
extractOpenAICacheReadTokens({
input_tokens_details: { cached_tokens: 42 },
prompt_tokens_details: { cached_tokens: 99 }
})
).toBe(42)
})
test('extractOpenAICacheReadTokens supports singular cached_token fallback fields', () => {
expect(
extractOpenAICacheReadTokens({
input_tokens_details: { cached_token: '17' }
})
).toBe(17)
expect(
extractOpenAICacheReadTokens({
prompt_tokens_details: { cached_token: 23 }
})
).toBe(23)
})
test('extractOpenAICacheReadTokens falls back to prompt_tokens_details.cached_tokens', () => {
expect(
extractOpenAICacheReadTokens({
prompt_tokens_details: { cached_tokens: '31' }
})
).toBe(31)
})
test('extractOpenAICacheReadTokens normalizes invalid values to zero', () => {
expect(extractOpenAICacheReadTokens()).toBe(0)
expect(
extractOpenAICacheReadTokens({
input_tokens_details: { cached_tokens: -5 }
})
).toBe(0)
expect(
extractOpenAICacheReadTokens({
input_tokens_details: { cached_tokens: 'abc' },
prompt_tokens_details: { cached_tokens: null }
})
).toBe(0)
})
test('calculateCacheHitRate uses cacheRead / (input + cacheRead + cacheCreate)', () => {
expect(calculateCacheHitRate(2048, 0, 17206)).toBe(10.64)
expect(calculateCacheHitRate(120, 80, 100)).toBe(40)
expect(calculateCacheHitRate(0, 0)).toBe(0)
})
test('getRequestDetailCacheMetrics exposes formula details for the request detail page', () => {
const metrics = getRequestDetailCacheMetrics({
endpoint: '/api/v1/messages',
accountType: 'claude-console',
inputTokens: 17206,
outputTokens: 51,
cacheReadTokens: 2048,
cacheCreateTokens: 0
})
expect(metrics.numerator).toBe(2048)
expect(metrics.denominator).toBe(19254)
expect(metrics.rate).toBe(10.64)
expect(metrics.cacheHitFormula).toBe(
'cacheReadTokens / (inputTokens + cacheReadTokens + cacheCreateTokens)'
)
})
test('calculateCacheHitRate uses the same input-side denominator for /openai/ requests', () => {
expect(
calculateCacheHitRate({
endpoint: '/openai/v1/responses',
inputTokens: 100,
cacheReadTokens: 60,
cacheCreateTokens: 0
})
).toBe(37.5)
expect(
calculateCacheHitRate({
endpoint: '/openai/v1/responses',
inputTokens: 0,
cacheReadTokens: 0
})
).toBe(0)
})
test('calculateCacheHitRate uses one denominator for azure records and claude compatibility routes', () => {
expect(
calculateCacheHitRate({
endpoint: '/azure/chat/completions',
accountType: 'azure-openai',
inputTokens: 100,
cacheReadTokens: 60,
cacheCreateTokens: 0
})
).toBe(37.5)
expect(
calculateCacheHitRate({
endpoint: '/openai/claude/v1/messages',
accountType: 'claude',
inputTokens: 100,
cacheReadTokens: 30,
cacheCreateTokens: 20
})
).toBe(20)
})
})