import { OpenAIClient } from './OpenAIClient' import { DEFAULT_TEMPERATURE, LLM_MAX_RETRIES } from './constants' import { InvokeError } from './errors' import type { InvokeOptions, InvokeResult, LLMClient, LLMConfig, Message, Tool } from './types' export type { InvokeError, InvokeOptions, InvokeResult, LLMClient, LLMConfig, Message, Tool } export function parseLLMConfig(config: LLMConfig): Required { // Runtime validation as defensive programming (types already guarantee these) if (!config.baseURL || !config.apiKey || !config.model) { throw new Error( '[PageAgent] LLM configuration required. Please provide: baseURL, apiKey, model. ' + 'See: https://alibaba.github.io/page-agent/#/docs/features/models' ) } return { baseURL: config.baseURL, apiKey: config.apiKey, model: config.model, temperature: config.temperature ?? DEFAULT_TEMPERATURE, maxRetries: config.maxRetries ?? LLM_MAX_RETRIES, customFetch: (config.customFetch ?? fetch).bind(globalThis), // fetch will be illegal unless bound } } export class LLM extends EventTarget { config: Required client: LLMClient constructor(config: LLMConfig) { super() this.config = parseLLMConfig(config) // Default to OpenAI client this.client = new OpenAIClient(this.config) } /** * - call llm api *once* * - invoke tool call *once* * - return the result of the tool */ async invoke( messages: Message[], tools: Record, abortSignal: AbortSignal, options?: InvokeOptions ): Promise { return await withRetry( async () => { const result = await this.client.invoke(messages, tools, abortSignal, options) return result }, // retry settings { maxRetries: this.config.maxRetries, onRetry: (attempt: number) => { this.dispatchEvent( new CustomEvent('retry', { detail: { attempt, maxAttempts: this.config.maxRetries } }) ) }, onError: (error: Error) => { this.dispatchEvent(new CustomEvent('error', { detail: { error } })) }, } ) } } async function withRetry( fn: () => Promise, settings: { maxRetries: number onRetry: (attempt: number) => void onError: (error: Error) => void } ): Promise { let attempt = 0 let lastError: Error | null = null while (attempt <= settings.maxRetries) { if (attempt > 0) { settings.onRetry(attempt) await new Promise((resolve) => setTimeout(resolve, 100)) } try { return await fn() } catch (error: unknown) { // do not retry if aborted by user if ((error as any)?.rawError?.name === 'AbortError') throw error console.error(error) settings.onError(error as Error) // do not retry if error is not retryable (InvokeError) if (error instanceof InvokeError && !error.retryable) throw error lastError = error as Error attempt++ await new Promise((resolve) => setTimeout(resolve, 100)) } } throw lastError! }