claude-flow
Version:
Enterprise-grade AI agent orchestration with ruv-swarm integration (Alpha Release)
476 lines (431 loc) • 14.1 kB
text/typescript
/**
* OpenAI Provider Implementation
* Supports GPT-4, GPT-3.5, and other OpenAI models
*/
import { BaseProvider } from './base-provider.js';
import {
LLMProvider,
LLMModel,
LLMRequest,
LLMResponse,
LLMStreamEvent,
ModelInfo,
ProviderCapabilities,
HealthCheckResult,
LLMProviderError,
RateLimitError,
AuthenticationError,
ModelNotFoundError,
} from './types.js';
interface OpenAIRequest {
model: string;
messages: Array<{
role: 'system' | 'user' | 'assistant' | 'function';
content: string;
name?: string;
function_call?: {
name: string;
arguments: string;
};
}>;
temperature?: number;
max_tokens?: number;
top_p?: number;
frequency_penalty?: number;
presence_penalty?: number;
stop?: string[];
stream?: boolean;
functions?: Array<{
name: string;
description: string;
parameters: any;
}>;
function_call?: 'auto' | 'none' | { name: string };
}
interface OpenAIResponse {
id: string;
object: string;
created: number;
model: string;
choices: Array<{
index: number;
message: {
role: string;
content: string | null;
function_call?: {
name: string;
arguments: string;
};
};
finish_reason: 'stop' | 'length' | 'function_call' | 'content_filter';
}>;
usage: {
prompt_tokens: number;
completion_tokens: number;
total_tokens: number;
};
}
interface OpenAIStreamChunk {
id: string;
object: string;
created: number;
model: string;
choices: Array<{
index: number;
delta: {
role?: string;
content?: string;
function_call?: {
name?: string;
arguments?: string;
};
};
finish_reason?: string;
}>;
}
export class OpenAIProvider extends BaseProvider {
readonly name: LLMProvider = 'openai';
readonly capabilities: ProviderCapabilities = {
supportedModels: [
'gpt-4-turbo-preview',
'gpt-4',
'gpt-4-32k',
'gpt-3.5-turbo',
'gpt-3.5-turbo-16k',
],
maxContextLength: {
'gpt-4-turbo-preview': 128000,
'gpt-4': 8192,
'gpt-4-32k': 32768,
'gpt-3.5-turbo': 4096,
'gpt-3.5-turbo-16k': 16384,
} as Record<LLMModel, number>,
maxOutputTokens: {
'gpt-4-turbo-preview': 4096,
'gpt-4': 4096,
'gpt-4-32k': 4096,
'gpt-3.5-turbo': 4096,
'gpt-3.5-turbo-16k': 4096,
} as Record<LLMModel, number>,
supportsStreaming: true,
supportsFunctionCalling: true,
supportsSystemMessages: true,
supportsVision: true, // GPT-4 with vision
supportsAudio: false,
supportsTools: true,
supportsFineTuning: true,
supportsEmbeddings: true,
supportsLogprobs: true,
supportsBatching: true,
rateLimit: {
requestsPerMinute: 3500,
tokensPerMinute: 90000,
concurrentRequests: 100,
},
pricing: {
'gpt-4-turbo-preview': {
promptCostPer1k: 0.01,
completionCostPer1k: 0.03,
currency: 'USD',
},
'gpt-4': {
promptCostPer1k: 0.03,
completionCostPer1k: 0.06,
currency: 'USD',
},
'gpt-4-32k': {
promptCostPer1k: 0.06,
completionCostPer1k: 0.12,
currency: 'USD',
},
'gpt-3.5-turbo': {
promptCostPer1k: 0.0005,
completionCostPer1k: 0.0015,
currency: 'USD',
},
'gpt-3.5-turbo-16k': {
promptCostPer1k: 0.003,
completionCostPer1k: 0.004,
currency: 'USD',
},
},
};
private baseUrl: string;
private headers: Record<string, string>;
protected async doInitialize(): Promise<void> {
if (!this.config.apiKey) {
throw new AuthenticationError('OpenAI API key is required', 'openai');
}
this.baseUrl = this.config.apiUrl || 'https://api.openai.com/v1';
this.headers = {
'Authorization': `Bearer ${this.config.apiKey}`,
'Content-Type': 'application/json',
};
// Add organization header if provided
if (this.config.providerOptions?.organization) {
this.headers['OpenAI-Organization'] = this.config.providerOptions.organization;
}
}
protected async doComplete(request: LLMRequest): Promise<LLMResponse> {
const openAIRequest: OpenAIRequest = {
model: this.mapToOpenAIModel(request.model || this.config.model),
messages: request.messages.map((msg) => ({
role: msg.role,
content: msg.content,
...(msg.name && { name: msg.name }),
...(msg.functionCall && { function_call: msg.functionCall }),
})),
temperature: request.temperature ?? this.config.temperature,
max_tokens: request.maxTokens ?? this.config.maxTokens,
top_p: request.topP ?? this.config.topP,
frequency_penalty: request.frequencyPenalty ?? this.config.frequencyPenalty,
presence_penalty: request.presencePenalty ?? this.config.presencePenalty,
stop: request.stopSequences ?? this.config.stopSequences,
stream: false,
};
// Add function calling if present
if (request.functions) {
openAIRequest.functions = request.functions;
openAIRequest.function_call = request.functionCall;
}
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), this.config.timeout || 60000);
try {
const response = await fetch(`${this.baseUrl}/chat/completions`, {
method: 'POST',
headers: this.headers,
body: JSON.stringify(openAIRequest),
signal: controller.signal,
});
clearTimeout(timeout);
if (!response.ok) {
await this.handleErrorResponse(response);
}
const data: OpenAIResponse = await response.json();
const choice = data.choices[0];
// Calculate cost
const model = request.model || this.config.model;
const pricing = this.capabilities.pricing![model];
const promptCost = (data.usage.prompt_tokens / 1000) * pricing.promptCostPer1k;
const completionCost = (data.usage.completion_tokens / 1000) * pricing.completionCostPer1k;
return {
id: data.id,
model: this.mapFromOpenAIModel(data.model),
provider: 'openai',
content: choice.message.content || '',
functionCall: choice.message.function_call,
usage: {
promptTokens: data.usage.prompt_tokens,
completionTokens: data.usage.completion_tokens,
totalTokens: data.usage.total_tokens,
},
cost: {
promptCost,
completionCost,
totalCost: promptCost + completionCost,
currency: 'USD',
},
finishReason: choice.finish_reason,
};
} catch (error) {
clearTimeout(timeout);
throw this.transformError(error);
}
}
protected async *doStreamComplete(request: LLMRequest): AsyncIterable<LLMStreamEvent> {
const openAIRequest: OpenAIRequest = {
model: this.mapToOpenAIModel(request.model || this.config.model),
messages: request.messages.map((msg) => ({
role: msg.role,
content: msg.content,
...(msg.name && { name: msg.name }),
...(msg.functionCall && { function_call: msg.functionCall }),
})),
temperature: request.temperature ?? this.config.temperature,
max_tokens: request.maxTokens ?? this.config.maxTokens,
top_p: request.topP ?? this.config.topP,
frequency_penalty: request.frequencyPenalty ?? this.config.frequencyPenalty,
presence_penalty: request.presencePenalty ?? this.config.presencePenalty,
stop: request.stopSequences ?? this.config.stopSequences,
stream: true,
};
if (request.functions) {
openAIRequest.functions = request.functions;
openAIRequest.function_call = request.functionCall;
}
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), (this.config.timeout || 60000) * 2);
try {
const response = await fetch(`${this.baseUrl}/chat/completions`, {
method: 'POST',
headers: this.headers,
body: JSON.stringify(openAIRequest),
signal: controller.signal,
});
if (!response.ok) {
await this.handleErrorResponse(response);
}
const reader = response.body!.getReader();
const decoder = new TextDecoder();
let buffer = '';
let totalPromptTokens = 0;
let totalCompletionTokens = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]') continue;
try {
const chunk: OpenAIStreamChunk = JSON.parse(data);
const delta = chunk.choices[0].delta;
if (delta.content) {
yield {
type: 'content',
delta: { content: delta.content },
};
}
if (delta.function_call) {
yield {
type: 'function_call',
delta: { functionCall: delta.function_call },
};
}
if (chunk.choices[0].finish_reason) {
// Estimate tokens for streaming
const promptTokens = this.estimateTokens(JSON.stringify(request.messages));
const completionTokens = Math.max(totalCompletionTokens, 100); // Minimum estimate
const model = request.model || this.config.model;
const pricing = this.capabilities.pricing![model];
const promptCost = (promptTokens / 1000) * pricing.promptCostPer1k;
const completionCost = (completionTokens / 1000) * pricing.completionCostPer1k;
yield {
type: 'done',
usage: {
promptTokens,
completionTokens,
totalTokens: promptTokens + completionTokens,
},
cost: {
promptCost,
completionCost,
totalCost: promptCost + completionCost,
currency: 'USD',
},
};
}
} catch (e) {
this.logger.warn('Failed to parse OpenAI stream chunk', { data, error: e });
}
}
}
}
} catch (error) {
clearTimeout(timeout);
throw this.transformError(error);
} finally {
clearTimeout(timeout);
}
}
async listModels(): Promise<LLMModel[]> {
return this.capabilities.supportedModels;
}
async getModelInfo(model: LLMModel): Promise<ModelInfo> {
return {
model,
name: model,
description: this.getModelDescription(model),
contextLength: this.capabilities.maxContextLength[model] || 4096,
maxOutputTokens: this.capabilities.maxOutputTokens[model] || 4096,
supportedFeatures: [
'chat',
'completion',
'function_calling',
...(model.includes('gpt-4') ? ['vision'] : []),
],
pricing: this.capabilities.pricing![model],
};
}
protected async doHealthCheck(): Promise<HealthCheckResult> {
try {
const response = await fetch(`${this.baseUrl}/models`, {
headers: this.headers,
});
if (!response.ok) {
throw new Error(`Health check failed: ${response.status}`);
}
return {
healthy: true,
timestamp: new Date(),
};
} catch (error) {
return {
healthy: false,
error: error instanceof Error ? error.message : 'Unknown error',
timestamp: new Date(),
};
}
}
private mapToOpenAIModel(model: LLMModel): string {
// Map our model names to OpenAI model names if needed
const modelMap: Record<string, string> = {
'gpt-4-turbo-preview': 'gpt-4-turbo-preview',
'gpt-4': 'gpt-4',
'gpt-4-32k': 'gpt-4-32k',
'gpt-3.5-turbo': 'gpt-3.5-turbo',
'gpt-3.5-turbo-16k': 'gpt-3.5-turbo-16k',
};
return modelMap[model] || model;
}
private mapFromOpenAIModel(model: string): LLMModel {
// Ensure the model is in our supported list
return this.capabilities.supportedModels.find((m) => m === model) || 'gpt-3.5-turbo';
}
private getModelDescription(model: LLMModel): string {
const descriptions: Record<string, string> = {
'gpt-4-turbo-preview': 'Latest GPT-4 Turbo model with improved performance',
'gpt-4': 'Most capable GPT-4 model for complex tasks',
'gpt-4-32k': 'GPT-4 with extended 32k context window',
'gpt-3.5-turbo': 'Fast and efficient model for most tasks',
'gpt-3.5-turbo-16k': 'GPT-3.5 Turbo with extended context',
};
return descriptions[model] || 'OpenAI language model';
}
private async handleErrorResponse(response: Response): Promise<void> {
const errorText = await response.text();
let errorData: any;
try {
errorData = JSON.parse(errorText);
} catch {
errorData = { error: { message: errorText } };
}
const message = errorData.error?.message || 'Unknown error';
switch (response.status) {
case 401:
throw new AuthenticationError(message, 'openai', errorData);
case 429:
const retryAfter = response.headers.get('retry-after');
throw new RateLimitError(
message,
'openai',
retryAfter ? parseInt(retryAfter) : undefined,
errorData
);
case 404:
throw new ModelNotFoundError(this.config.model, 'openai', errorData);
default:
throw new LLMProviderError(
message,
`OPENAI_${response.status}`,
'openai',
response.status,
response.status >= 500,
errorData
);
}
}
}