This commit is contained in:
Oleg Maslov
2026-09-02 10:14:22 +02:00
parent 0c3e2ead3b
commit b20b138fe4
771 changed files with 161561 additions and 9027 deletions

View File

@@ -1,5 +1,9 @@
import { describe, it, expect, vi } from 'vitest';
import { openaiChat } from '../src/providers/openai-compat.js';
import {
isIncompleteCompletionError,
openaiChat,
parseOpenAiTextCompletion,
} from '../src/providers/openai-compat.js';
import type { ResolvedModel } from '../src/model-router.js';
const resolved: ResolvedModel = {
@@ -9,10 +13,13 @@ const resolved: ResolvedModel = {
baseUrl: 'https://api.example.com/v1',
};
function okBody(content = 'hi') {
function okBody(content: string | null = 'hi', finishReason: string | null | 'missing' = 'stop') {
return new Response(
JSON.stringify({
choices: [{ message: { content } }],
choices: [{
message: { content },
...(finishReason === 'missing' ? {} : { finish_reason: finishReason }),
}],
model: 'gpt-4o-mini',
usage: { prompt_tokens: 12, completion_tokens: 5 },
}),
@@ -30,6 +37,117 @@ describe('openaiChat', () => {
expect(res.usage).toEqual({ input_tokens: 12, output_tokens: 5 });
});
it.each(['missing', null, 'length', 'content_filter', 'tool_calls'])(
'rejects a 200 response with non-final finish reason %s without replay',
async (finishReason) => {
const fetchImpl = vi.fn(async () => okBody('Partial content', finishReason)) as unknown as typeof fetch;
await expect(openaiChat(
resolved,
[{ role: 'user', content: 'hey' }],
undefined,
{ fetchImpl, sleepImpl: noSleep },
)).rejects.toMatchObject({
code: 'INCOMPLETE_COMPLETION',
usage: { inputTokens: 12, outputTokens: 5 },
message: expect.stringMatching(/finish_reason=.*partial content was rejected/i),
});
expect(fetchImpl).toHaveBeenCalledOnce();
},
);
it.each([null, '', ' '])('rejects stop with unusable assistant text %s', async (content) => {
const fetchImpl = vi.fn(async () => okBody(content, 'stop')) as unknown as typeof fetch;
await expect(openaiChat(
resolved,
[{ role: 'user', content: 'hey' }],
undefined,
{ fetchImpl, sleepImpl: noSleep },
)).rejects.toMatchObject({
code: 'INCOMPLETE_COMPLETION',
usage: { inputTokens: 12, outputTokens: 5 },
});
expect(fetchImpl).toHaveBeenCalledOnce();
});
it('rejects stop with missing assistant text or tool calls without replay', async () => {
const payloads = [
{ choices: [{ finish_reason: 'stop', message: {} }] },
{
choices: [{
finish_reason: 'stop',
message: {
content: 'Text plus an unsupported tool call.',
tool_calls: [{ id: 'call_1' }],
},
}],
},
];
for (const payload of payloads) {
const fetchImpl = vi.fn(async () => new Response(JSON.stringify({
...payload,
model: 'gpt-4o-mini',
usage: { prompt_tokens: 12, completion_tokens: 5 },
}), { status: 200 })) as unknown as typeof fetch;
await expect(openaiChat(
resolved,
[{ role: 'user', content: 'hey' }],
undefined,
{ fetchImpl, sleepImpl: noSleep },
)).rejects.toMatchObject({ code: 'INCOMPLETE_COMPLETION' });
expect(fetchImpl).toHaveBeenCalledOnce();
}
});
it('classifies an empty paid choice set as incomplete and preserves usage', async () => {
const fetchImpl = vi.fn(async () => new Response(JSON.stringify({
choices: [],
model: 'gpt-4o-mini',
usage: { prompt_tokens: 12, completion_tokens: 5 },
}), { status: 200 })) as unknown as typeof fetch;
await expect(openaiChat(
resolved,
[{ role: 'user', content: 'hey' }],
undefined,
{ fetchImpl, sleepImpl: noSleep },
)).rejects.toMatchObject({
code: 'INCOMPLETE_COMPLETION',
usage: { inputTokens: 12, outputTokens: 5 },
message: expect.stringMatching(/missing completion choice/i),
});
expect(fetchImpl).toHaveBeenCalledOnce();
});
it('rejects a null JSON response without replay', async () => {
const fetchImpl = vi.fn(async () => new Response('null', { status: 200 })) as unknown as typeof fetch;
await expect(openaiChat(
resolved,
[{ role: 'user', content: 'hey' }],
undefined,
{ fetchImpl, sleepImpl: noSleep },
)).rejects.toMatchObject({ code: 'INCOMPLETE_COMPLETION' });
expect(fetchImpl).toHaveBeenCalledOnce();
});
it('classifies malformed JSON from a successful HTTP response as incomplete without replay', async () => {
const fetchImpl = vi.fn(async () => new Response('{', { status: 200 })) as unknown as typeof fetch;
await expect(openaiChat(
resolved,
[{ role: 'user', content: 'hey' }],
undefined,
{ fetchImpl, sleepImpl: noSleep },
)).rejects.toMatchObject({ code: 'INCOMPLETE_COMPLETION' });
expect(fetchImpl).toHaveBeenCalledOnce();
});
it('passes an AbortSignal (timeout) to fetch', async () => {
const fetchImpl = vi.fn(async (_url: string | URL, init?: RequestInit) => {
expect(init?.signal).toBeInstanceOf(AbortSignal);
@@ -92,3 +210,40 @@ describe('openaiChat', () => {
expect((fetchImpl as unknown as ReturnType<typeof vi.fn>)).toHaveBeenCalledTimes(1);
});
});
describe('parseOpenAiTextCompletion', () => {
it('returns complete text and normalized token/cost usage', () => {
expect(parseOpenAiTextCompletion({
choices: [{ finish_reason: 'stop', message: { content: 'Complete.' } }],
model: 'test-model',
usage: { prompt_tokens: 9, completion_tokens: 4, total_cost: 0.0123 },
})).toEqual({
content: 'Complete.',
model: 'test-model',
usage: { inputTokens: 9, outputTokens: 4, totalCostUsd: 0.0123 },
});
});
it('preserves paid usage on an incomplete provider error payload', () => {
try {
parseOpenAiTextCompletion({
error: { message: 'provider interrupted' },
usage: { prompt_tokens: 8, completion_tokens: 3, total_cost: 0.004 },
});
throw new Error('expected parser to reject');
} catch (error) {
expect(isIncompleteCompletionError(error)).toBe(true);
expect(error).toMatchObject({
code: 'INCOMPLETE_COMPLETION',
usage: { inputTokens: 8, outputTokens: 3, totalCostUsd: 0.004 },
});
}
});
it.each([
{ finish_reason: 'stop', message: { content: 'text', refusal: 'blocked' } },
{ finish_reason: 'stop', message: { content: 'text', tool_calls: { malformed: true } } },
])('rejects terminal payload mismatches', (choice) => {
expect(() => parseOpenAiTextCompletion({ choices: [choice] })).toThrow(/rejected/i);
});
});