ai-functions
Version:
Core AI primitives for building intelligent applications
394 lines (345 loc) • 11.5 kB
text/typescript
/**
* 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'
}