139 lines
3.7 KiB
TypeScript
139 lines
3.7 KiB
TypeScript
import { describe, it, expect, vi } from 'vitest';
|
|
import { OpenAIClient } from './openai.js';
|
|
|
|
const { mockCreate, mockOpenAIConstructor } = vi.hoisted(() => {
|
|
const mockCreate = vi.fn().mockResolvedValue({
|
|
choices: [{ message: { content: 'Hello from GPT!' }, finish_reason: 'stop' }],
|
|
usage: { prompt_tokens: 10, completion_tokens: 5 },
|
|
});
|
|
const mockOpenAIConstructor = vi.fn().mockImplementation(() => ({
|
|
chat: {
|
|
completions: {
|
|
create: mockCreate,
|
|
},
|
|
},
|
|
}));
|
|
return { mockCreate, mockOpenAIConstructor };
|
|
});
|
|
|
|
vi.mock('openai', () => ({
|
|
default: mockOpenAIConstructor,
|
|
}));
|
|
|
|
describe('OpenAIClient', () => {
|
|
it('sets request timeout and disables SDK retries', () => {
|
|
new OpenAIClient({
|
|
apiKey: 'test-key',
|
|
model: 'gpt-4o',
|
|
});
|
|
|
|
expect(mockOpenAIConstructor).toHaveBeenCalledWith(expect.objectContaining({
|
|
timeout: 20_000,
|
|
maxRetries: 0,
|
|
}));
|
|
});
|
|
|
|
it('sends messages and returns response', async () => {
|
|
const client = new OpenAIClient({
|
|
apiKey: 'test-key',
|
|
model: 'gpt-4o',
|
|
});
|
|
|
|
const response = await client.chat({
|
|
messages: [{ role: 'user', content: 'Hello' }],
|
|
});
|
|
|
|
expect(response.content).toBe('Hello from GPT!');
|
|
expect(response.stopReason).toBe('end_turn');
|
|
expect(response.usage.inputTokens).toBe(10);
|
|
expect(response.usage.outputTokens).toBe(5);
|
|
});
|
|
});
|
|
|
|
describe('OpenAIClient tool use', () => {
|
|
it('passes tools to API and parses tool_calls response', async () => {
|
|
mockCreate.mockResolvedValueOnce({
|
|
choices: [{
|
|
message: {
|
|
content: null,
|
|
tool_calls: [{
|
|
id: 'call_1',
|
|
type: 'function',
|
|
function: { name: 'shell.exec', arguments: '{"command":"ls"}' },
|
|
}],
|
|
},
|
|
finish_reason: 'tool_calls',
|
|
}],
|
|
usage: { prompt_tokens: 20, completion_tokens: 15 },
|
|
});
|
|
|
|
const client = new OpenAIClient({
|
|
apiKey: 'test-key',
|
|
model: 'gpt-4o',
|
|
});
|
|
|
|
const response = await client.chat({
|
|
messages: [{ role: 'user', content: 'list files' }],
|
|
tools: [{
|
|
name: 'shell.exec',
|
|
description: 'Run shell command',
|
|
input_schema: { type: 'object', properties: { command: { type: 'string' } }, required: ['command'] },
|
|
}],
|
|
});
|
|
|
|
expect(response.stopReason).toBe('tool_use');
|
|
expect(response.toolCalls).toHaveLength(1);
|
|
expect(response.toolCalls![0]).toEqual({
|
|
id: 'call_1',
|
|
name: 'shell.exec',
|
|
args: { command: 'ls' },
|
|
});
|
|
});
|
|
|
|
it('maps finish_reason "tool_calls" with empty tool_calls to end_turn', async () => {
|
|
mockCreate.mockResolvedValueOnce({
|
|
choices: [{
|
|
message: {
|
|
content: 'I tried to call a tool but none matched.',
|
|
tool_calls: [],
|
|
},
|
|
finish_reason: 'tool_calls',
|
|
}],
|
|
usage: { prompt_tokens: 15, completion_tokens: 10 },
|
|
});
|
|
|
|
const client = new OpenAIClient({
|
|
apiKey: 'test-key',
|
|
model: 'gpt-4o',
|
|
});
|
|
|
|
const response = await client.chat({
|
|
messages: [{ role: 'user', content: 'do something' }],
|
|
});
|
|
|
|
expect(response.stopReason).toBe('end_turn');
|
|
expect(response.toolCalls).toBeUndefined();
|
|
});
|
|
|
|
it('maps finish_reason "length" to max_tokens', async () => {
|
|
mockCreate.mockResolvedValueOnce({
|
|
choices: [{
|
|
message: { content: 'Truncated output...' },
|
|
finish_reason: 'length',
|
|
}],
|
|
usage: { prompt_tokens: 100, completion_tokens: 4096 },
|
|
});
|
|
|
|
const client = new OpenAIClient({
|
|
apiKey: 'test-key',
|
|
model: 'gpt-4o',
|
|
});
|
|
|
|
const response = await client.chat({
|
|
messages: [{ role: 'user', content: 'write a long essay' }],
|
|
});
|
|
|
|
expect(response.stopReason).toBe('max_tokens');
|
|
});
|
|
});
|