UNPKG

ai-functions

Version:

Core AI primitives for building intelligent applications

394 lines (345 loc) 11.5 kB
/** * Execution Context for AI Functions * * Standalone context module with batch processing and budget tracking features. * Settings flow from environment -> global context -> local context. * * @example * ```ts * // Set global defaults (from environment or initialization) * configure({ * provider: 'anthropic', * model: 'claude-sonnet-4-20250514', * batchMode: 'auto', // 'auto' | 'immediate' | 'deferred' * }) * * // Or use execution context for specific operations * await withContext({ provider: 'openai', model: 'gpt-4o' }, async () => { * const titles = await list`10 blog titles` * return titles.map(title => write`blog post: ${title}`) * }) * ``` * * @packageDocumentation */ import type { BatchProvider } from './batch-queue.js' import type { RequestContext as IRequestContext, ModelPricing } from './budget.js' // ============================================================================ // Core Types (from template.ts equivalent) // ============================================================================ /** * Common options for all AI functions */ export interface FunctionOptions { /** Model to use (e.g., 'claude-opus-4-5', 'gpt-5-1', 'gemini-3-pro') */ model?: string /** Thinking level: 'none', 'low', 'medium', 'high', or token budget number */ thinking?: 'none' | 'low' | 'medium' | 'high' | number /** Temperature (0-2) */ temperature?: number /** Maximum tokens to generate */ maxTokens?: number /** System prompt */ system?: string /** Processing mode */ mode?: 'default' | 'background' } // ============================================================================ // Extended Types for Batch Processing // ============================================================================ /** Batch execution mode */ export type BatchMode = | 'auto' // Smart selection: immediate < flexThreshold, flex < batchThreshold, batch above | 'immediate' // Execute immediately (concurrent requests, full price) | 'flex' // Use flex processing (faster than batch, ~50% discount, minutes) | 'deferred' // Always use provider batch API (50% discount, up to 24hr) /** Budget configuration for context */ export interface ContextBudgetConfig { /** Maximum total tokens allowed */ maxTokens?: number /** Maximum cost in USD */ maxCost?: number /** Alert thresholds as fractions (e.g., [0.5, 0.8, 1.0]) */ alertThresholds?: number[] /** Custom pricing for models not in default pricing table */ customPricing?: Record<string, ModelPricing> } /** * Execution context with batch processing and budget features. */ export interface ExecutionContext extends FunctionOptions { /** Provider to use (for model resolution) */ provider?: BatchProvider | string /** Batch execution mode */ batchMode?: BatchMode /** Minimum items to use flex processing (for 'auto' mode, default: 5) */ flexThreshold?: number /** Minimum items to use batch API (for 'auto' mode, default: 500) */ batchThreshold?: number /** Webhook URL for batch completion notifications */ webhookUrl?: string /** Custom metadata for batch jobs */ metadata?: Record<string, unknown> /** Budget configuration for tracking and limits */ budget?: ContextBudgetConfig /** Request context for tracing */ requestContext?: IRequestContext } // ============================================================================ // Global Context // ============================================================================ let globalContext: ExecutionContext = {} /** * Configure global defaults for AI functions * * @example * ```ts * configure({ * model: 'claude-sonnet-4-20250514', * provider: 'anthropic', * batchMode: 'auto', * batchThreshold: 5, * }) * ``` */ export function configure(context: ExecutionContext): void { globalContext = { ...globalContext, ...context } } /** * Get the current global context */ export function getGlobalContext(): ExecutionContext { return { ...globalContext } } /** * Reset global context to defaults */ export function resetContext(): void { globalContext = {} } // ============================================================================ // Async Local Storage for Execution Context // ============================================================================ // Use AsyncLocalStorage if available (Node.js), otherwise fallback to global let asyncLocalStorage: { getStore: () => ExecutionContext | undefined run: <T>(store: ExecutionContext, callback: () => T) => T } | null = null // Lazy initialization of AsyncLocalStorage let asyncLocalStorageInitialized = false // Initialize synchronously if possible (for Node.js environments) if (typeof process !== 'undefined' && process.versions?.node) { import('async_hooks') .then(({ AsyncLocalStorage }) => { asyncLocalStorage = new AsyncLocalStorage<ExecutionContext>() asyncLocalStorageInitialized = true }) .catch(() => { asyncLocalStorageInitialized = true }) } // ============================================================================ // Environment Defaults // ============================================================================ function getEnvContext(): ExecutionContext { if (typeof process === 'undefined') return {} const context: ExecutionContext = {} // Model defaults if (process.env['AI_MODEL']) { context.model = process.env['AI_MODEL'] } // Provider defaults if (process.env['AI_PROVIDER']) { context.provider = process.env['AI_PROVIDER'] } else if (process.env['ANTHROPIC_API_KEY'] && !process.env['OPENAI_API_KEY']) { context.provider = 'anthropic' } else if (process.env['OPENAI_API_KEY']) { context.provider = 'openai' } else if (process.env['CLOUDFLARE_API_TOKEN']) { context.provider = 'cloudflare' } else if (process.env['AWS_ACCESS_KEY_ID']) { context.provider = 'bedrock' } return context } function getEnvBatchContext(): Partial<ExecutionContext> { if (typeof process === 'undefined') return {} const context: Partial<ExecutionContext> = {} // Batch mode if (process.env['AI_BATCH_MODE']) { context.batchMode = process.env['AI_BATCH_MODE'] as BatchMode } // Flex threshold (when to start using flex processing) if (process.env['AI_FLEX_THRESHOLD']) { context.flexThreshold = parseInt(process.env['AI_FLEX_THRESHOLD'], 10) } // Batch threshold (when to switch from flex to full batch) if (process.env['AI_BATCH_THRESHOLD']) { context.batchThreshold = parseInt(process.env['AI_BATCH_THRESHOLD'], 10) } // Webhook URL if (process.env['AI_BATCH_WEBHOOK_URL']) { context.webhookUrl = process.env['AI_BATCH_WEBHOOK_URL'] } return context } // ============================================================================ // Context Functions // ============================================================================ /** * Get the current execution context * Merges: environment defaults -> global context -> local context */ export function getContext(): ExecutionContext { const envContext = getEnvContext() const envBatchContext = getEnvBatchContext() const localContext = asyncLocalStorage?.getStore() return { ...envContext, ...envBatchContext, ...globalContext, ...localContext, } } /** * Run a function with a specific execution context * * @example * ```ts * const posts = await withContext({ provider: 'openai', batchMode: 'deferred' }, async () => { * const titles = await list`10 blog titles` * return titles.map(title => write`blog post: ${title}`) * }) * ``` */ export function withContext<T>( context: ExecutionContext, fn: () => T | Promise<T> ): T | Promise<T> { const mergedContext = { ...getContext(), ...context } if (asyncLocalStorage) { return asyncLocalStorage.run(mergedContext, fn) } // Fallback: temporarily modify global context const previousContext = globalContext globalContext = mergedContext try { return fn() } finally { globalContext = previousContext } } // ============================================================================ // Context Helpers // ============================================================================ /** * Get the effective model from context */ export function getModel(): string { const ctx = getContext() return ctx.model || 'sonnet' } /** * Get the effective provider from context (typed as BatchProvider) */ export function getProvider(): BatchProvider { const ctx = getContext() return (ctx.provider || 'openai') as BatchProvider } // ============================================================================ // Batch-Specific Context Helpers // ============================================================================ /** * Get the effective batch mode from context */ export function getBatchMode(): BatchMode { const ctx = getContext() return ctx.batchMode || 'auto' } /** * Get the flex threshold from context (minimum items to use flex) * Default: 5 items */ export function getFlexThreshold(): number { const ctx = getContext() return ctx.flexThreshold || 5 } /** * Get the batch threshold from context (minimum items to use full batch) * Default: 500 items */ export function getBatchThreshold(): number { const ctx = getContext() return ctx.batchThreshold || 500 } /** Execution tier for processing */ export type ExecutionTier = 'immediate' | 'flex' | 'batch' /** * Determine the execution tier for a given number of items * * Auto mode tiers: * - immediate: < flexThreshold (default 5) - concurrent requests, full price * - flex: flexThreshold to batchThreshold (5-500) - ~50% discount, minutes * - batch: >= batchThreshold (500+) - 50% discount, up to 24hr * * @example * ```ts * getExecutionTier(3) // 'immediate' (< 5) * getExecutionTier(50) // 'flex' (5-500) * getExecutionTier(1000) // 'batch' (500+) * ``` */ export function getExecutionTier(itemCount: number): ExecutionTier { const mode = getBatchMode() switch (mode) { case 'immediate': return 'immediate' case 'flex': return 'flex' case 'deferred': return 'batch' case 'auto': default: { const flexThreshold = getFlexThreshold() const batchThreshold = getBatchThreshold() if (itemCount < flexThreshold) { return 'immediate' } else if (itemCount < batchThreshold) { return 'flex' } else { return 'batch' } } } } /** * Check if we should use the batch API for a given number of items * * @deprecated Use {@link getExecutionTier} instead for more granular control. * This function will be removed in a future major version. * * @param itemCount - Number of items to process * @returns true if batch or flex tier should be used, false for immediate * * @example * ```ts * // Deprecated usage: * if (shouldUseBatchAPI(items.length)) { ... } * * // Recommended: * const tier = getExecutionTier(items.length) * if (tier === 'batch' || tier === 'flex') { ... } * ``` */ export function shouldUseBatchAPI(itemCount: number): boolean { const tier = getExecutionTier(itemCount) return tier === 'flex' || tier === 'batch' } /** * Check if flex processing is available for the current provider * Only OpenAI and AWS Bedrock support flex processing currently */ export function isFlexAvailable(): boolean { const provider = getProvider() return provider === 'openai' || provider === 'bedrock' || provider === 'google' }