import type { LanguageModelV1 } from 'ai'; import type { ProviderInfo, ProviderConfig, ModelInfo } from './types'; import type { IProviderSetting } from '~/types/model'; import { createOpenAI } from '@ai-sdk/openai'; import { LLMManager } from './manager'; /** Default timeout for model listing API calls (5 seconds) */ const MODEL_FETCH_TIMEOUT = 5_000; export abstract class BaseProvider implements ProviderInfo { abstract name: string; abstract staticModels: ModelInfo[]; abstract config: ProviderConfig; cachedDynamicModels?: { cacheId: string; models: ModelInfo[]; }; getApiKeyLink?: string; labelForGetApiKey?: string; icon?: string; /** * Convert Cloudflare Env bindings to a plain Record. * Useful because provider methods expect Record but * Cloudflare Workers pass an Env interface. */ protected convertEnvToRecord(env?: Env): Record { if (!env) { return {}; } return Object.entries(env).reduce( (acc, [key, value]) => { acc[key] = String(value); return acc; }, {} as Record, ); } /** * Rewrite localhost / 127.0.0.1 URLs to host.docker.internal when * running inside Docker. Only applies on the server side. */ protected resolveDockerUrl(baseUrl: string, serverEnv?: Record): string { const isDocker = process?.env?.RUNNING_IN_DOCKER === 'true' || serverEnv?.RUNNING_IN_DOCKER === 'true'; if (!isDocker) { return baseUrl; } return baseUrl.replace('localhost', 'host.docker.internal').replace('127.0.0.1', 'host.docker.internal'); } /** * Create an AbortSignal that times out after the given milliseconds. * Used to prevent model-listing fetches from hanging indefinitely. */ protected createTimeoutSignal(ms: number = MODEL_FETCH_TIMEOUT): AbortSignal { return AbortSignal.timeout(ms); } getProviderBaseUrlAndKey(options: { apiKeys?: Record; providerSettings?: IProviderSetting; serverEnv?: Record; defaultBaseUrlKey: string; defaultApiTokenKey: string; }) { const { apiKeys, providerSettings, serverEnv, defaultBaseUrlKey, defaultApiTokenKey } = options; let settingsBaseUrl = providerSettings?.baseUrl; const manager = LLMManager.getInstance(); if (settingsBaseUrl && settingsBaseUrl.length == 0) { settingsBaseUrl = undefined; } const baseUrlKey = this.config.baseUrlKey || defaultBaseUrlKey; let baseUrl = settingsBaseUrl || serverEnv?.[baseUrlKey] || process?.env?.[baseUrlKey] || manager.env?.[baseUrlKey] || this.config.baseUrl; if (baseUrl && baseUrl.endsWith('/')) { baseUrl = baseUrl.slice(0, -1); } const apiTokenKey = this.config.apiTokenKey || defaultApiTokenKey; const apiKey = apiKeys?.[this.name] || serverEnv?.[apiTokenKey] || process?.env?.[apiTokenKey] || manager.env?.[apiTokenKey]; return { baseUrl, apiKey, }; } getModelsFromCache(options: { apiKeys?: Record; providerSettings?: Record; serverEnv?: Record; }): ModelInfo[] | null { if (!this.cachedDynamicModels) { return null; } const cacheKey = this.cachedDynamicModels.cacheId; const generatedCacheKey = this.getDynamicModelsCacheKey(options); if (cacheKey !== generatedCacheKey) { this.cachedDynamicModels = undefined; return null; } return this.cachedDynamicModels.models; } getDynamicModelsCacheKey(options: { apiKeys?: Record; providerSettings?: Record; serverEnv?: Record; }) { // Only include provider-relevant env keys, not the entire server environment const relevantEnvKeys = [this.config.baseUrlKey, this.config.apiTokenKey].filter(Boolean) as string[]; const relevantEnv: Record = {}; for (const key of relevantEnvKeys) { if (options.serverEnv?.[key]) { relevantEnv[key] = options.serverEnv[key]; } } return JSON.stringify({ apiKeys: options.apiKeys?.[this.name], providerSettings: options.providerSettings?.[this.name], serverEnv: relevantEnv, }); } storeDynamicModels( options: { apiKeys?: Record; providerSettings?: Record; serverEnv?: Record; }, models: ModelInfo[], ) { const cacheId = this.getDynamicModelsCacheKey(options); this.cachedDynamicModels = { cacheId, models, }; } // Declare the optional getDynamicModels method getDynamicModels?( apiKeys?: Record, settings?: IProviderSetting, serverEnv?: Record, ): Promise; abstract getModelInstance(options: { model: string; serverEnv?: Env; apiKeys?: Record; providerSettings?: Record; }): LanguageModelV1; } type OptionalApiKey = string | undefined; export function getOpenAILikeModel(baseURL: string, apiKey: OptionalApiKey, model: string) { const openai = createOpenAI({ baseURL, apiKey, }); return openai(model); }