Files
waooplus/tests/unit/user-api/model-llm-protocol-probe.test.ts
2026-03-08 17:10:06 +08:00

199 lines
7.1 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest'
const resolveOpenAICompatClientConfigMock = vi.hoisted(() =>
vi.fn(async () => ({
providerId: 'openai-compatible:node-1',
baseUrl: 'https://compat.example.com/v1',
apiKey: 'sk-test',
})),
)
vi.mock('@/lib/model-gateway/openai-compat/common', () => ({
resolveOpenAICompatClientConfig: resolveOpenAICompatClientConfigMock,
}))
import { probeModelLlmProtocol } from '@/lib/user-api/model-llm-protocol-probe'
describe('user-api model llm protocol probe', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('returns responses protocol when responses endpoint succeeds', async () => {
const fetchMock = vi.fn(async () => new Response(JSON.stringify({ id: 'resp_1' }), { status: 200 }))
vi.stubGlobal('fetch', fetchMock)
const result = await probeModelLlmProtocol({
userId: 'user-1',
providerId: 'openai-compatible:node-1',
modelId: 'gpt-4.1-mini',
})
expect(result.success).toBe(true)
if (!result.success) return
expect(result.protocol).toBe('responses')
expect(fetchMock).toHaveBeenCalledTimes(1)
const firstCall = fetchMock.mock.calls[0] as unknown[] | undefined
expect(String(firstCall?.[0])).toBe('https://compat.example.com/v1/responses')
})
it('returns chat-completions when responses is unsupported and chat succeeds', async () => {
const fetchMock = vi.fn(async (input: unknown) => {
const url = String(input)
if (url.endsWith('/responses')) return new Response('not found', { status: 404 })
if (url.endsWith('/chat/completions')) return new Response(JSON.stringify({ id: 'chatcmpl_1' }), { status: 200 })
return new Response('unexpected', { status: 500 })
})
vi.stubGlobal('fetch', fetchMock)
const result = await probeModelLlmProtocol({
userId: 'user-1',
providerId: 'openai-compatible:node-1',
modelId: 'gpt-4.1-mini',
})
expect(result.success).toBe(true)
if (!result.success) return
expect(result.protocol).toBe('chat-completions')
expect(result.traces.map((trace) => trace.endpoint)).toEqual(['responses', 'chat-completions'])
})
it('returns chat-completions when responses is rate limited but chat succeeds', async () => {
const fetchMock = vi.fn(async (input: unknown) => {
const url = String(input)
if (url.endsWith('/responses')) return new Response('rate limit', { status: 429 })
if (url.endsWith('/chat/completions')) return new Response(JSON.stringify({ id: 'chatcmpl_1' }), { status: 200 })
return new Response('unexpected', { status: 500 })
})
vi.stubGlobal('fetch', fetchMock)
const result = await probeModelLlmProtocol({
userId: 'user-1',
providerId: 'openai-compatible:node-1',
modelId: 'gpt-4.1-mini',
})
expect(result.success).toBe(true)
if (!result.success) return
expect(result.protocol).toBe('chat-completions')
expect(result.traces[0]?.status).toBe(429)
expect(result.traces[1]?.status).toBe(200)
})
it('treats responses 5xx with not-implemented style message as unsupported', async () => {
const fetchMock = vi.fn(async (input: unknown) => {
const url = String(input)
if (url.endsWith('/responses')) {
return new Response(JSON.stringify({
error: {
message: 'not implemented (request id: x)',
code: 'local:convert_request_failed',
},
}), { status: 500 })
}
if (url.endsWith('/chat/completions')) {
return new Response(JSON.stringify({ id: 'chatcmpl_1' }), { status: 200 })
}
return new Response('unexpected', { status: 500 })
})
vi.stubGlobal('fetch', fetchMock)
const result = await probeModelLlmProtocol({
userId: 'user-1',
providerId: 'openai-compatible:node-1',
modelId: 'gpt-4.1-mini',
})
expect(result.success).toBe(true)
if (!result.success) return
expect(result.protocol).toBe('chat-completions')
})
it('treats responses 400 with unsupported keywords as unsupported', async () => {
const fetchMock = vi.fn(async (input: unknown) => {
const url = String(input)
if (url.endsWith('/responses')) {
return new Response(JSON.stringify({ error: { message: 'unknown endpoint /responses' } }), { status: 400 })
}
if (url.endsWith('/chat/completions')) {
return new Response(JSON.stringify({ id: 'chatcmpl_1' }), { status: 200 })
}
return new Response('unexpected', { status: 500 })
})
vi.stubGlobal('fetch', fetchMock)
const result = await probeModelLlmProtocol({
userId: 'user-1',
providerId: 'openai-compatible:node-1',
modelId: 'gpt-4.1-mini',
})
expect(result.success).toBe(true)
if (!result.success) return
expect(result.protocol).toBe('chat-completions')
})
it('returns chat-completions when responses 422 has no unsupported keywords but chat succeeds', async () => {
const fetchMock = vi.fn(async (input: unknown) => {
const url = String(input)
if (url.endsWith('/responses')) {
return new Response(JSON.stringify({ error: { message: 'invalid payload' } }), { status: 422 })
}
if (url.endsWith('/chat/completions')) {
return new Response(JSON.stringify({ id: 'chatcmpl_1' }), { status: 200 })
}
return new Response('unexpected', { status: 500 })
})
vi.stubGlobal('fetch', fetchMock)
const result = await probeModelLlmProtocol({
userId: 'user-1',
providerId: 'openai-compatible:node-1',
modelId: 'gpt-4.1-mini',
})
expect(result.success).toBe(true)
if (!result.success) return
expect(result.protocol).toBe('chat-completions')
expect(result.traces[0]?.status).toBe(422)
expect(result.traces[1]?.status).toBe(200)
})
it('returns auth failure when responses and chat both return 401', async () => {
const fetchMock = vi.fn(async () => new Response('unauthorized', { status: 401 }))
vi.stubGlobal('fetch', fetchMock)
const result = await probeModelLlmProtocol({
userId: 'user-1',
providerId: 'openai-compatible:node-1',
modelId: 'gpt-4.1-mini',
})
expect(result.success).toBe(false)
if (result.success) return
expect(result.code).toBe('PROBE_AUTH_FAILED')
expect(fetchMock).toHaveBeenCalledTimes(2)
})
it('returns chat-completions when responses auth fails but chat succeeds', async () => {
const fetchMock = vi.fn(async (input: unknown) => {
const url = String(input)
if (url.endsWith('/responses')) return new Response('unauthorized', { status: 401 })
if (url.endsWith('/chat/completions')) return new Response(JSON.stringify({ id: 'chatcmpl_1' }), { status: 200 })
return new Response('unexpected', { status: 500 })
})
vi.stubGlobal('fetch', fetchMock)
const result = await probeModelLlmProtocol({
userId: 'user-1',
providerId: 'openai-compatible:node-1',
modelId: 'gpt-4.1-mini',
})
expect(result.success).toBe(true)
if (!result.success) return
expect(result.protocol).toBe('chat-completions')
expect(fetchMock).toHaveBeenCalledTimes(2)
})
})