/** * OpenAI Client implementation * @note This client is only for demonstrating how to implement a LLM client. * @note Use OpenAILenientClient instead. */ import { InvokeError, InvokeErrorType } from './errors' import type { InvokeResult, LLMClient, LLMConfig, Message, Tool } from './types' import { modelPatch, zodToOpenAITool } from './utils' /** * @deprecated Use OpenAILenientClient instead. */ export class OpenAIClient implements LLMClient { config: LLMConfig constructor(config: LLMConfig) { this.config = config } async invoke( messages: Message[], tools: Record, abortSignal?: AbortSignal ): Promise { // 1. Convert tools to OpenAI format const openaiTools = Object.entries(tools).map(([name, tool]) => zodToOpenAITool(name, tool)) // 2. Call API let response: Response try { response = await fetch(`${this.config.baseURL}/chat/completions`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this.config.apiKey}`, }, body: JSON.stringify( modelPatch({ model: this.config.model, temperature: this.config.temperature, max_tokens: this.config.maxTokens, messages, tools: openaiTools, // tool_choice: 'required', tool_choice: { type: 'function', function: { name: 'AgentOutput' } }, // model specific params // reasoning_effort: 'minimal', // verbosity: 'low', parallel_tool_calls: false, }) ), signal: abortSignal, }) } catch (error: unknown) { // Network error throw new InvokeError(InvokeErrorType.NETWORK_ERROR, 'Network request failed', error) } // 3. Handle HTTP errors if (!response.ok) { const errorData = await response.json().catch() const errorMessage = (errorData as { error?: { message?: string } }).error?.message || response.statusText if (response.status === 401 || response.status === 403) { throw new InvokeError( InvokeErrorType.AUTH_ERROR, `Authentication failed: ${errorMessage}`, errorData ) } if (response.status === 429) { throw new InvokeError( InvokeErrorType.RATE_LIMIT, `Rate limit exceeded: ${errorMessage}`, errorData ) } if (response.status >= 500) { throw new InvokeError( InvokeErrorType.SERVER_ERROR, `Server error: ${errorMessage}`, errorData ) } throw new InvokeError( InvokeErrorType.UNKNOWN, `HTTP ${response.status}: ${errorMessage}`, errorData ) } const data = await response.json() // 4. Check finish_reason const choice = data.choices?.[0] if (!choice) { throw new InvokeError(InvokeErrorType.UNKNOWN, 'No choices in response', data) } switch (choice.finish_reason) { case 'tool_calls': // ✅ Normal break case 'length': // ⚠️ Token limit reached throw new InvokeError( InvokeErrorType.CONTEXT_LENGTH, 'Response truncated: max tokens reached', data ) case 'content_filter': // ❌ Content filtered throw new InvokeError( InvokeErrorType.CONTENT_FILTER, 'Content filtered by safety system', data ) case 'stop': // ❌ Did not call tool (we require tool call) throw new InvokeError(InvokeErrorType.NO_TOOL_CALL, 'Model did not call any tool', data) default: throw new InvokeError( InvokeErrorType.UNKNOWN, `Unexpected finish_reason: ${choice.finish_reason}`, data ) } // 5. Parse tool call const toolCall = choice.message?.tool_calls?.[0] if (!toolCall) { throw new InvokeError(InvokeErrorType.NO_TOOL_CALL, 'No tool call found in response', data) } const toolName = toolCall.function.name const tool = tools[toolName] if (!tool) { throw new InvokeError(InvokeErrorType.UNKNOWN, `Tool ${toolName} not found`, data) } // 6. Parse and validate arguments let toolArgs: unknown try { toolArgs = JSON.parse(toolCall.function.arguments) } catch (e) { throw new InvokeError(InvokeErrorType.INVALID_TOOL_ARGS, 'Invalid JSON in tool arguments', e) } // Validate against zod schema const validation = tool.inputSchema.safeParse(toolArgs) if (!validation.success) { throw new InvokeError( InvokeErrorType.INVALID_TOOL_ARGS, `Tool arguments validation failed: ${validation.error.message}`, validation.error ) } // 7. Execute tool let toolResult: unknown try { toolResult = await tool.execute(validation.data) } catch (e) { throw new InvokeError( InvokeErrorType.TOOL_EXECUTION_ERROR, `Tool execution failed: ${(e as Error).message}`, e ) } // 8. Return result (including cache tokens) return { toolCall: { // id: toolCall.id, name: toolName, args: validation.data as Record, }, toolResult, usage: { promptTokens: data.usage?.prompt_tokens ?? 0, completionTokens: data.usage?.completion_tokens ?? 0, totalTokens: data.usage?.total_tokens ?? 0, cachedTokens: data.usage?.prompt_tokens_details?.cached_tokens, reasoningTokens: data.usage?.completion_tokens_details?.reasoning_tokens, }, rawResponse: data, } } }