/** * OpenAiProvider — transport-error surfacing and reasoning_content capture. * * Covers the two gate prompt-ranking failure modes reproduced live 2026-07-23: * - a LiteLLM/vLLM HTTP 500 must reject (not parse as an empty completion), * - a reasoning ("thinking") model that emits its answer under * reasoning_content with content=null must still yield that text. */ import { describe, it, expect, beforeEach, vi } from 'vitest'; let mockStatus = 200; let mockBody = '{}'; vi.mock('node:http', () => { const request = vi.fn((_opts: unknown, cb?: unknown) => { const res = { statusCode: mockStatus, on: (event: string, handler: (d?: unknown) => void) => { if (event === 'data') handler(Buffer.from(mockBody)); if (event === 'end') handler(); return res; }, }; if (typeof cb === 'function') (cb as (r: unknown) => void)(res); return { on: vi.fn().mockReturnThis(), write: vi.fn(), end: vi.fn() }; }); return { default: { request }, request }; }); const { OpenAiProvider } = await import('../src/providers/openai.js'); function provider(): InstanceType { return new OpenAiProvider({ apiKey: 'k', baseUrl: 'http://localhost:9' }); } describe('OpenAiProvider transport + reasoning handling', () => { beforeEach(() => { mockStatus = 200; mockBody = '{}'; }); it('rejects on HTTP >= 400 with the status + body (not a masked empty completion)', async () => { mockStatus = 500; mockBody = JSON.stringify({ error: { message: 'litellm.InternalServerError: Connection error' } }); await expect( provider().complete({ messages: [{ role: 'user', content: 'hi' }], maxTokens: 8 }), ).rejects.toThrow(/OpenAI HTTP 500/); }); it('falls back to reasoning_content when content is null (thinking model)', async () => { mockBody = JSON.stringify({ choices: [{ message: { content: null, reasoning_content: '{"selectedNames":["x"]}' }, finish_reason: 'length' }], }); const r = await provider().complete({ messages: [{ role: 'user', content: 'hi' }], maxTokens: 8 }); expect(r.content).toContain('selectedNames'); expect(r.finishReason).toBe('length'); }); it('reads reasoning_content from provider_specific_fields too', async () => { mockBody = JSON.stringify({ choices: [{ message: { content: '', provider_specific_fields: { reasoning_content: 'thought-answer' } } }], }); const r = await provider().complete({ messages: [{ role: 'user', content: 'hi' }], maxTokens: 8 }); expect(r.content).toBe('thought-answer'); }); it('prefers real content over reasoning_content when both are present', async () => { mockBody = JSON.stringify({ choices: [{ message: { content: 'real', reasoning_content: 'thinking' }, finish_reason: 'stop' }], }); const r = await provider().complete({ messages: [{ role: 'user', content: 'hi' }], maxTokens: 8 }); expect(r.content).toBe('real'); }); });