/** * Cost-Tiered Pipeline * * Demonstrates: * - Running the same four-stage `runTasks()` pipeline twice * - Assigning different models per agent within one pipeline * - Capturing token usage per model via `onTrace` * - Estimating USD cost from provider-specific model pricing * * Run: * npx tsx examples/patterns/cost-tiered-pipeline.ts * * Prerequisites: * Set one of: * - ANTHROPIC_API_KEY * - OPENAI_API_KEY * * Optional: * - LLM_PROVIDER=anthropic|openai * - ANTHROPIC_BASE_URL for Anthropic-compatible relays * - OPENAI_BASE_URL for OpenAI-compatible relays */ import { OpenMultiAgent } from '../../src/index.js' import type { AgentConfig, OrchestratorEvent, TokenUsage, TraceEvent } from '../../src/types.js' // --------------------------------------------------------------------------- // Pricing // --------------------------------------------------------------------------- const PRICING_AS_OF = '2026-04-24' const PRICING = { // Anthropic + OpenAI API pricing, as of 2026-04-24, USD per 1M tokens 'claude-opus-4-7': { input: 5, output: 25 }, 'claude-sonnet-4-6': { input: 3, output: 15 }, 'claude-haiku-4-5': { input: 1, output: 5 }, 'gpt-5.5': { input: 5, output: 30 }, 'gpt-5.4': { input: 2.5, output: 15 }, 'gpt-5.4-mini': { input: 0.75, output: 4.5 }, } as const type PricedModel = keyof typeof PRICING type UsageByModel = Partial> type ProviderId = 'anthropic' | 'openai' type ProviderConfig = Pick type AgentRunSummary = Map type PipelineAssignments = { researcher: ProviderConfig classifier: ProviderConfig drafter: ProviderConfig reviewer: ProviderConfig } type ScenarioResult = { label: string assignments: PipelineAssignments agentResults: AgentRunSummary usageByModel: UsageByModel totalTokenUsage: TokenUsage costUsd: number elapsedMs: number finalBrief: string } // --------------------------------------------------------------------------- // Topic // --------------------------------------------------------------------------- const TOPIC = 'Launch a developer-focused AI code review assistant for 200-800 engineer software teams' // --------------------------------------------------------------------------- // Prompts // --------------------------------------------------------------------------- const RESEARCHER_PROMPT = `You are a product researcher preparing source material for a launch brief. Focus on: - target buyer and user personas - pain points in code review workflows - market landscape and competitive differentiators - adoption risks and rollout constraints Write structured markdown in 220-300 words. Be concrete and specific.` const CLASSIFIER_PROMPT = `You are a product strategist turning raw research into decision-ready structure. Organize the upstream research into four sections: 1. Audience 2. Risks 3. Differentiators 4. Open Questions Write 180-250 words in markdown. Keep it crisp and managerial.` const DRAFTER_PROMPT = `You are a senior product marketer writing a launch brief. Use the prior task outputs from shared memory and produce a polished brief with: - executive summary - target audience - product positioning - go-to-market risks - recommended launch angle Write 380-500 words in markdown.` const REVIEWER_PROMPT = `You are an executive reviewer. Rewrite the brief for clarity, internal consistency, and sharper prioritization. Keep the strongest claims, remove repetition, and make the recommendation decisive. Return the final brief in 350-450 words using markdown headings.` // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function zeroUsage(): TokenUsage { return { input_tokens: 0, output_tokens: 0 } } function addUsage(a: TokenUsage, b: TokenUsage): TokenUsage { return { input_tokens: a.input_tokens + b.input_tokens, output_tokens: a.output_tokens + b.output_tokens, } } function isPricedModel(model: string): model is PricedModel { return model in PRICING } function createUsageCollector(): { usageByModel: UsageByModel handleTrace: (event: TraceEvent) => void } { const usageByModel: UsageByModel = {} function handleTrace(event: TraceEvent): void { if (event.type !== 'llm_call' || !isPricedModel(event.model)) return usageByModel[event.model] = addUsage(usageByModel[event.model] ?? zeroUsage(), event.tokens) } return { usageByModel, handleTrace } } function estimateCostUsd(usageByModel: UsageByModel): number { return Object.entries(usageByModel).reduce((sum, [model, usage]) => { if (!usage || !isPricedModel(model)) return sum const pricing = PRICING[model] return sum + ( (usage.input_tokens / 1_000_000) * pricing.input + (usage.output_tokens / 1_000_000) * pricing.output ) }, 0) } function formatUsd(value: number): string { return `$${value.toFixed(4)}` } function formatSeconds(ms: number): string { return `${(ms / 1000).toFixed(2)}s` } function formatPercent(value: number): string { return `${(value * 100).toFixed(1)}%` } function inferProvider(): ProviderId { if (process.env.OPENAI_API_KEY) return 'openai' if (process.env.ANTHROPIC_API_KEY) return 'anthropic' throw new Error('Set OPENAI_API_KEY or ANTHROPIC_API_KEY to run this example.') } function getSelectedProvider(): ProviderId { const requested = process.env.LLM_PROVIDER?.trim().toLowerCase() if (!requested) return inferProvider() if (requested === 'anthropic' || requested === 'openai') { return requested } throw new Error( `Unsupported LLM_PROVIDER="${process.env.LLM_PROVIDER}". Use one of: anthropic, openai.`, ) } function getProviderConfigs(provider: ProviderId): { baselineLabel: string tieredLabel: string endpointLabel: string flagship: ProviderConfig mid: ProviderConfig cheap: ProviderConfig } { switch (provider) { case 'openai': { const apiKey = process.env.OPENAI_API_KEY if (!apiKey) { throw new Error('LLM_PROVIDER=openai requires OPENAI_API_KEY.') } const baseURL = process.env.OPENAI_BASE_URL return { baselineLabel: 'All GPT-5.5 baseline', tieredLabel: 'Tiered GPT mix', endpointLabel: baseURL ?? 'OpenAI official API', flagship: { provider: 'openai', model: 'gpt-5.5', apiKey, baseURL, }, mid: { provider: 'openai', model: 'gpt-5.4', apiKey, baseURL, }, cheap: { provider: 'openai', model: 'gpt-5.4-mini', apiKey, baseURL, }, } } case 'anthropic': default: { const apiKey = process.env.ANTHROPIC_API_KEY if (!apiKey) { throw new Error('LLM_PROVIDER=anthropic requires ANTHROPIC_API_KEY.') } const baseURL = process.env.ANTHROPIC_BASE_URL return { baselineLabel: 'All Opus baseline', tieredLabel: 'Tiered Claude mix', endpointLabel: baseURL ?? 'Anthropic official API', flagship: { provider: 'anthropic', model: 'claude-opus-4-7', apiKey, baseURL, }, mid: { provider: 'anthropic', model: 'claude-sonnet-4-6', apiKey, baseURL, }, cheap: { provider: 'anthropic', model: 'claude-haiku-4-5', apiKey, baseURL, }, } } } } function createProgressHandler(label: string): (event: OrchestratorEvent) => void { return (event: OrchestratorEvent): void => { if (event.type === 'task_start') { console.log(` [START] ${label} :: ${event.task ?? ''} → ${event.agent ?? ''}`) } if (event.type === 'task_complete') { console.log(` [DONE] ${label} :: ${event.task ?? ''}`) } } } function createAgents(assignments: PipelineAssignments): AgentConfig[] { return [ { name: 'researcher', ...assignments.researcher, systemPrompt: RESEARCHER_PROMPT, maxTurns: 2, temperature: 0.2, }, { name: 'classifier', ...assignments.classifier, systemPrompt: CLASSIFIER_PROMPT, maxTurns: 2, temperature: 0.2, }, { name: 'drafter', ...assignments.drafter, systemPrompt: DRAFTER_PROMPT, maxTurns: 2, temperature: 0.3, }, { name: 'reviewer', ...assignments.reviewer, systemPrompt: REVIEWER_PROMPT, maxTurns: 2, temperature: 0.2, }, ] } function describeAssignments(assignments: PipelineAssignments): string { return [ `researcher=${assignments.researcher.model}`, `classifier=${assignments.classifier.model}`, `drafter=${assignments.drafter.model}`, `reviewer=${assignments.reviewer.model}`, ].join(', ') } function printAgentTokenSummary(agentResults: AgentRunSummary): void { console.log('Agent Token Summary') console.log('-'.repeat(60)) for (const [name, run] of agentResults) { const icon = run.success ? 'OK ' : 'FAIL' const tokens = `in:${run.tokenUsage.input_tokens} out:${run.tokenUsage.output_tokens}` console.log(` [${icon}] ${name.padEnd(12)} ${tokens}`) } console.log() } function printModelCostBreakdown(usageByModel: UsageByModel): void { console.log('Model Cost Breakdown') console.log('-'.repeat(60)) console.log('| Model | Input | Output | USD cost |') console.log('|-------|------:|-------:|---------:|') for (const [model, usage] of Object.entries(usageByModel)) { if (!usage || !isPricedModel(model)) continue const costUsd = (usage.input_tokens / 1_000_000) * PRICING[model].input + (usage.output_tokens / 1_000_000) * PRICING[model].output console.log( `| ${model} | ${usage.input_tokens} | ${usage.output_tokens} | ${formatUsd(costUsd)} |`, ) } console.log() } function printComparisonTable(runs: ScenarioResult[]): void { console.log('Comparison') console.log('-'.repeat(60)) console.log('| Run | Input | Output | USD cost | Wall time |') console.log('|-----|------:|-------:|---------:|----------:|') for (const run of runs) { console.log( `| ${run.label} | ${run.totalTokenUsage.input_tokens} | ${run.totalTokenUsage.output_tokens} | ${formatUsd(run.costUsd)} | ${formatSeconds(run.elapsedMs)} |`, ) } console.log() } function printFinalSummaryTable(runs: ScenarioResult[]): void { console.log('Final Summary') console.log('-'.repeat(60)) console.log('| Run | Models | Input | Output | USD cost | Wall time |') console.log('|-----|--------|------:|-------:|---------:|----------:|') for (const run of runs) { console.log( `| ${run.label} | ${describeAssignments(run.assignments)} | ${run.totalTokenUsage.input_tokens} | ${run.totalTokenUsage.output_tokens} | ${formatUsd(run.costUsd)} | ${formatSeconds(run.elapsedMs)} |`, ) } console.log() } function printScenarioDetails(run: ScenarioResult): void { console.log('='.repeat(60)) console.log(run.label.toUpperCase()) console.log('='.repeat(60)) console.log(`Models: ${describeAssignments(run.assignments)}`) console.log( `Tokens — input: ${run.totalTokenUsage.input_tokens}, output: ${run.totalTokenUsage.output_tokens}`, ) console.log(`Estimated USD cost — ${formatUsd(run.costUsd)}`) console.log(`Wall time — ${formatSeconds(run.elapsedMs)}`) console.log() printAgentTokenSummary(run.agentResults) printModelCostBreakdown(run.usageByModel) console.log('Final Brief') console.log('-'.repeat(60)) console.log(run.finalBrief) console.log() } async function runScenario( label: string, assignments: PipelineAssignments, tasks: Array<{ title: string description: string assignee: string dependsOn?: string[] }>, ): Promise { const agents = createAgents(assignments) const usageCollector = createUsageCollector() const orchestrator = new OpenMultiAgent({ defaultProvider: assignments.reviewer.provider, defaultModel: assignments.reviewer.model, defaultBaseURL: assignments.reviewer.baseURL, defaultApiKey: assignments.reviewer.apiKey, onProgress: createProgressHandler(label), onTrace: usageCollector.handleTrace, }) const team = orchestrator.createTeam(`cost-tiered-pipeline-${label.toLowerCase().replaceAll(/[^a-z0-9]+/g, '-')}`, { name: `cost-tiered-pipeline-${label.toLowerCase().replaceAll(/[^a-z0-9]+/g, '-')}`, agents, sharedMemory: true, }) console.log(`Run: ${label}`) console.log(`Models: ${describeAssignments(assignments)}`) console.log() const startedAt = performance.now() const result = await orchestrator.runTasks(team, tasks) const elapsedMs = performance.now() - startedAt if (!result.success) { console.error(`${label} failed.`) for (const [name, run] of result.agentResults) { if (!run.success) { console.error(` ${name}: ${run.output}`) } } process.exit(1) } const finalResult = result.agentResults.get('reviewer') if (!finalResult?.success) { console.error(`${label} did not produce a final reviewer output.`) process.exit(1) } return { label, assignments, agentResults: result.agentResults, usageByModel: usageCollector.usageByModel, totalTokenUsage: result.totalTokenUsage, costUsd: estimateCostUsd(usageCollector.usageByModel), elapsedMs, finalBrief: finalResult.output, } } // --------------------------------------------------------------------------- // Provider resolution // --------------------------------------------------------------------------- const selectedProvider = getSelectedProvider() const providerConfigs = getProviderConfigs(selectedProvider) // --------------------------------------------------------------------------- // Tasks — fixed four-stage pipeline // --------------------------------------------------------------------------- const tasks = [ { title: 'Research launch context', description: `Research ${TOPIC}. Focus on buyer pain points, review bottlenecks, team adoption concerns, competitor positioning, and measurable product value.`, assignee: 'researcher', }, { title: 'Classify findings', description: `Read the research from shared memory and classify it into audience, risks, differentiators, and open questions. Preserve concrete details and avoid generic filler.`, assignee: 'classifier', dependsOn: ['Research launch context'], }, { title: 'Draft launch brief', description: `Using the structured findings, draft a substantial launch brief for leadership review. The brief should be decision-ready and exceed 300 words.`, assignee: 'drafter', dependsOn: ['Classify findings'], }, { title: 'Review final brief', description: `Review the drafted brief, tighten the positioning, and return the final version leadership should read.`, assignee: 'reviewer', dependsOn: ['Draft launch brief'], }, ] // --------------------------------------------------------------------------- // Scenario assignments // --------------------------------------------------------------------------- const baselineAssignments: PipelineAssignments = { researcher: providerConfigs.flagship, classifier: providerConfigs.flagship, drafter: providerConfigs.flagship, reviewer: providerConfigs.flagship, } const tieredAssignments: PipelineAssignments = { researcher: providerConfigs.cheap, classifier: providerConfigs.cheap, drafter: providerConfigs.mid, reviewer: providerConfigs.flagship, } // --------------------------------------------------------------------------- // Run // --------------------------------------------------------------------------- console.log('Cost-Tiered Pipeline') console.log('='.repeat(60)) console.log(`Provider: ${selectedProvider}`) console.log(`Endpoint: ${providerConfigs.endpointLabel}`) console.log(`Pricing as of: ${PRICING_AS_OF}`) console.log(`Topic: ${TOPIC}`) console.log('Pipeline: researcher -> classifier -> drafter -> reviewer') console.log() const baselineRun = await runScenario(providerConfigs.baselineLabel, baselineAssignments, tasks) console.log('-'.repeat(60)) const tieredRun = await runScenario(providerConfigs.tieredLabel, tieredAssignments, tasks) // --------------------------------------------------------------------------- // Output // --------------------------------------------------------------------------- console.log('\n' + '='.repeat(60)) printComparisonTable([baselineRun, tieredRun]) const savings = 1 - (tieredRun.costUsd / baselineRun.costUsd) console.log(`Tiered savings vs baseline — ${formatPercent(savings)}`) console.log() printScenarioDetails(baselineRun) printScenarioDetails(tieredRun) console.log('='.repeat(60)) printFinalSummaryTable([baselineRun, tieredRun]) if (savings < 0.4) { console.error(`ASSERTION FAILED: tiered savings ${Math.round(savings * 100)}% < 40%`) process.exit(1) } console.log(`Savings assertion — OK (${formatPercent(savings)} >= 40.0%)`) console.log('\nDone.')