import { libx } from 'libx.js/build/bundles/essentials.js'; import { IConvMessage, IPromptConfig } from './FeedoxAI'; /** * Client for the Ask API Worker * Replaces FeedoxAI with our own Cloudflare Worker */ export class WorkerClient { constructor(public options?: Partial) { this.options = { ...new WorkerClientOptions(), ...options }; } /** * Stream a prompt through the worker API */ public async streamPrompt( workspaceId: string, promptId: string, variables: any, messages: IConvMessage[], config?: IPromptConfig ): Promise { const url = `${this.options.baseUrl}/completion`; const payload = { messages, systemPrompt: config?.systemPrompt, config: { provider: config?.provider || this.options.defaultProvider, model: config?.model || this.options.defaultModel, temperature: config?.temperature ?? 0.7, maxTokens: config?.maxTokens, topP: config?.topP, frequencyPenalty: config?.frequencyPenalty, presencePenalty: config?.presencePenalty, stream: true, user: config?.user, } }; libx.log.v('WorkerClient: streaming request to', url, payload.config); const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders(config), }, body: JSON.stringify(payload) }); if (!response.ok) { const error = await response.text(); throw new Error(`Worker API error: ${error}`); } return response.body!; } /** * Run a prompt without streaming (sync mode) */ public async runPrompt( workspaceId: string, promptId: string, variables: any, messages: IConvMessage[], config?: IPromptConfig ): Promise { const url = `${this.options.baseUrl}/completion`; const payload = { messages, systemPrompt: config?.systemPrompt, config: { provider: config?.provider || this.options.defaultProvider, model: config?.model || this.options.defaultModel, temperature: config?.temperature ?? 0.7, maxTokens: config?.maxTokens, topP: config?.topP, frequencyPenalty: config?.frequencyPenalty, presencePenalty: config?.presencePenalty, stream: false, user: config?.user, } }; libx.log.v('WorkerClient: sync request to', url, payload.config); const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders(config), }, body: JSON.stringify(payload) }); if (!response.ok) { const error = await response.text(); throw new Error(`Worker API error: ${error}`); } const data = await response.json(); return data.content || ''; } /** * Get authentication headers based on provider * If ASK_API_KEY is present, use it for remote key management * Otherwise, fall back to individual vendor keys */ private getAuthHeaders(config?: IPromptConfig): Record { const headers: Record = {}; // If Ask API key is present, use it and skip individual vendor keys if (this.options.askApiKey) { headers['Authorization'] = `Bearer ${this.options.askApiKey}`; libx.log.v('WorkerClient: Using Ask API key for authentication'); return headers; } // Fallback: Map all provider API keys from environment to headers const provider = config?.provider; if (provider === 'openai' && this.options.openaiApiKey) { headers['x-openai-api-key'] = this.options.openaiApiKey; } else if (provider === 'anthropic' && this.options.claudeApiKey) { headers['x-claude-api-key'] = this.options.claudeApiKey; } else if (provider === 'groq' && this.options.groqApiKey) { headers['x-groq-api-key'] = this.options.groqApiKey; } else if (provider === 'google' && this.options.googleApiKey) { headers['x-google-api-key'] = this.options.googleApiKey; } else if (provider === 'mistral' && this.options.mistralApiKey) { headers['x-mistral-api-key'] = this.options.mistralApiKey; } else if (provider === 'openrouter' && this.options.openrouterApiKey) { headers['x-openrouter-api-key'] = this.options.openrouterApiKey; } else if (provider === 'cohere' && this.options.cohereApiKey) { headers['x-cohere-api-key'] = this.options.cohereApiKey; } else if (provider === 'xai' && this.options.xaiApiKey) { headers['x-xai-api-key'] = this.options.xaiApiKey; } else if (provider === 'deepseek' && this.options.deepseekApiKey) { headers['x-deepseek-api-key'] = this.options.deepseekApiKey; } else if (provider === 'ai21' && this.options.ai21ApiKey) { headers['x-ai21-api-key'] = this.options.ai21ApiKey; } else if (provider === 'cloudflare') { if (this.options.cloudflareApiKey) { headers['x-cloudflare-api-key'] = this.options.cloudflareApiKey; } if (this.options.cloudflareAccountId) { headers['x-cloudflare-account-id'] = this.options.cloudflareAccountId; } } return headers; } } /** * Configuration options for the WorkerClient */ export class WorkerClientOptions { /** Base URL of the worker API */ baseUrl = process.env.WORKER_API_URL || 'http://localhost:59898/v1'; /** Default provider to use if not specified */ defaultProvider = process.env.DEFAULT_PROVIDER || 'openai'; /** Default model to use if not specified */ defaultModel = process.env.DEFAULT_MODEL || 'gpt-3.5-turbo'; /** Ask API key for remote key management (if present, individual vendor keys are ignored) */ askApiKey = process.env.ASK_API_KEY; /** API keys (optional - can also be set in worker environment) */ openaiApiKey = process.env.OPENAI_API_KEY; claudeApiKey = process.env.CLAUDE_API_KEY; groqApiKey = process.env.GROQ_API_KEY; googleApiKey = process.env.GOOGLE_AI_API_KEY; mistralApiKey = process.env.MISTRAL_API_KEY; openrouterApiKey = process.env.OPENROUTER_API_KEY; cohereApiKey = process.env.COHERE_API_KEY; xaiApiKey = process.env.XAI_API_KEY; deepseekApiKey = process.env.DEEPSEEK_API_KEY; ai21ApiKey = process.env.AI21_API_KEY; cloudflareApiKey = process.env.CLOUDFLARE_API_KEY; cloudflareAccountId = process.env.CLOUDFLARE_ACCOUNT_ID; }