feat: add OpenAI client for fallback support

Implements ModelClient interface with OpenAI SDK to support GPT models
as fallback when local inference is unavailable. Includes tests with
mocked OpenAI API responses.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
William Valentin
2026-02-02 21:10:46 -08:00
parent 7338b390ef
commit 633cfcf713
3 changed files with 88 additions and 0 deletions
+1
View File
@@ -1,2 +1,3 @@
export { AnthropicClient, type AnthropicClientConfig } from './anthropic.js';
export { OpenAIClient, type OpenAIClientConfig } from './openai.js';
export type { Message, ChatRequest, ChatResponse, ModelClient } from './types.js';
+33
View File
@@ -0,0 +1,33 @@
import { describe, it, expect, vi } from 'vitest';
import { OpenAIClient } from './openai.js';
vi.mock('openai', () => ({
default: vi.fn().mockImplementation(() => ({
chat: {
completions: {
create: vi.fn().mockResolvedValue({
choices: [{ message: { content: 'Hello from GPT!' }, finish_reason: 'stop' }],
usage: { prompt_tokens: 10, completion_tokens: 5 },
}),
},
},
})),
}));
describe('OpenAIClient', () => {
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('stop');
expect(response.usage.inputTokens).toBe(10);
expect(response.usage.outputTokens).toBe(5);
});
});
+54
View File
@@ -0,0 +1,54 @@
import OpenAI from 'openai';
import type { ChatRequest, ChatResponse, ModelClient } from './types.js';
export interface OpenAIClientConfig {
apiKey?: string;
model: string;
maxTokens?: number;
baseURL?: string;
}
export class OpenAIClient implements ModelClient {
private client: OpenAI;
private model: string;
private defaultMaxTokens: number;
constructor(config: OpenAIClientConfig) {
this.client = new OpenAI({
apiKey: config.apiKey,
baseURL: config.baseURL,
});
this.model = config.model;
this.defaultMaxTokens = config.maxTokens ?? 4096;
}
async chat(request: ChatRequest): Promise<ChatResponse> {
const messages: OpenAI.ChatCompletionMessageParam[] = [];
if (request.system) {
messages.push({ role: 'system', content: request.system });
}
for (const msg of request.messages) {
messages.push({ role: msg.role, content: msg.content });
}
const response = await this.client.chat.completions.create({
model: this.model,
max_tokens: request.maxTokens ?? this.defaultMaxTokens,
messages,
});
const choice = response.choices[0];
const content = choice?.message?.content ?? '';
return {
content,
stopReason: choice?.finish_reason ?? 'stop',
usage: {
inputTokens: response.usage?.prompt_tokens ?? 0,
outputTokens: response.usage?.completion_tokens ?? 0,
},
};
}
}