From d36a490333fec26200cd278e95a97a8adca53213 Mon Sep 17 00:00:00 2001 From: Tom Aylott Date: Tue, 11 Nov 2025 16:59:04 -0500 Subject: [PATCH 1/6] Add prompt-caching examples for Effect AI - Add typescript/effect-ai/src/prompt-caching/user-message-cache.ts - Demonstrates cache_control using options.openrouter.cacheControl in Prompt - Shows Effect.gen pattern with Layer-based dependency injection - Critical configuration: stream_options.include_usage in model config layer - Evidence-based verification via response.usage.cachedInputTokens --- docs/prompt-caching.md | 1 + typescript/effect-ai/README.md | 48 +++++++ typescript/effect-ai/package.json | 21 +++ .../effect-ai/src/prompt-caching/README.md | 121 +++++++++++++++++ .../src/prompt-caching/multi-message-cache.ts | 115 ++++++++++++++++ .../src/prompt-caching/no-cache-control.ts | 107 +++++++++++++++ .../src/prompt-caching/user-message-cache.ts | 124 ++++++++++++++++++ typescript/effect-ai/tsconfig.json | 16 +++ typescript/package.json | 3 +- 9 files changed, 555 insertions(+), 1 deletion(-) create mode 100644 typescript/effect-ai/README.md create mode 100644 typescript/effect-ai/package.json create mode 100644 typescript/effect-ai/src/prompt-caching/README.md create mode 100644 typescript/effect-ai/src/prompt-caching/multi-message-cache.ts create mode 100644 typescript/effect-ai/src/prompt-caching/no-cache-control.ts create mode 100644 typescript/effect-ai/src/prompt-caching/user-message-cache.ts create mode 100644 typescript/effect-ai/tsconfig.json diff --git a/docs/prompt-caching.md b/docs/prompt-caching.md index 919d00d..9973b95 100644 --- a/docs/prompt-caching.md +++ b/docs/prompt-caching.md @@ -189,3 +189,4 @@ See ecosystem-specific examples: - **TypeScript + fetch**: [typescript/fetch/src/prompt-caching/](../typescript/fetch/src/prompt-caching/) - **AI SDK v5** (Vercel): [typescript/ai-sdk-v5/src/prompt-caching/](../typescript/ai-sdk-v5/src/prompt-caching/) +- **Effect AI** (@effect/ai): [typescript/effect-ai/src/prompt-caching/](../typescript/effect-ai/src/prompt-caching/) diff --git a/typescript/effect-ai/README.md b/typescript/effect-ai/README.md new file mode 100644 index 0000000..f847c90 --- /dev/null +++ b/typescript/effect-ai/README.md @@ -0,0 +1,48 @@ +# Effect-TS AI Examples + +Examples using Effect-TS with @effect/ai and @effect/ai-openrouter for type-safe, composable AI operations. + +## Prerequisites + +- Bun runtime: `curl -fsSL https://bun.sh/install | bash` +- `OPENROUTER_API_KEY` environment variable + +## Running Examples + +```bash +# From monorepo root (typescript/) +bun examples + +# Or from this workspace +cd effect-ai +bun examples +``` + +## Features + +- [prompt-caching.ts](./src/prompt-caching.ts) - Anthropic caching with Effect patterns + +### Key Configuration + +**CRITICAL**: The Effect AI example requires: +```typescript +config: { + stream_options: { include_usage: true } +} +``` + +Without this, `usage.cachedInputTokens` will be undefined in the response. + +### Effect Patterns Demonstrated + +- `Effect.gen` for generator-based composition +- Layer-based dependency injection +- Type-safe error handling +- Evidence-based validation + +## Dependencies + +- `@openrouter-examples/shared` - Shared constants (LARGE_SYSTEM_PROMPT) and types +- `@effect/ai` - Effect AI abstractions +- `@effect/ai-openrouter` - OpenRouter provider for Effect AI +- `effect` - Effect-TS core library diff --git a/typescript/effect-ai/package.json b/typescript/effect-ai/package.json new file mode 100644 index 0000000..b485208 --- /dev/null +++ b/typescript/effect-ai/package.json @@ -0,0 +1,21 @@ +{ + "name": "@openrouter-examples/effect-ai", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "examples": "bun run src/prompt-caching/user-message-cache.ts && bun run src/prompt-caching/multi-message-cache.ts && bun run src/prompt-caching/no-cache-control.ts", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@openrouter-examples/shared": "workspace:*", + "@effect/ai": "^0.32.1", + "@effect/ai-openrouter": "^0.6.0", + "@effect/platform": "^0.93.0", + "@effect/platform-bun": "^0.83.0", + "effect": "^3.19.3" + }, + "devDependencies": { + "@types/bun": "latest" + } +} diff --git a/typescript/effect-ai/src/prompt-caching/README.md b/typescript/effect-ai/src/prompt-caching/README.md new file mode 100644 index 0000000..02725f6 --- /dev/null +++ b/typescript/effect-ai/src/prompt-caching/README.md @@ -0,0 +1,121 @@ +# Anthropic Prompt Caching Examples (Effect AI) + +This directory contains examples demonstrating Anthropic's prompt caching feature via OpenRouter using @effect/ai and @effect/ai-openrouter. + +## What is Prompt Caching? + +Anthropic's prompt caching allows you to cache large portions of your prompts to: +- **Reduce costs** - Cached tokens cost significantly less +- **Improve latency** - Cached content is processed faster +- **Enable larger contexts** - Use more context without proportional cost increases + +Cache TTL: 5 minutes for ephemeral caches + +## Examples + +### User Message Cache (`user-message-cache.ts`) +Cache large context in user messages using Effect AI: +```bash +bun run typescript/effect-ai/src/prompt-caching/user-message-cache.ts +``` + +**Pattern**: User message with `options.openrouter.cacheControl` using Effect.gen + +## How to Use with Effect AI + +```typescript +import * as OpenRouterClient from '@effect/ai-openrouter/OpenRouterClient'; +import * as OpenRouterLanguageModel from '@effect/ai-openrouter/OpenRouterLanguageModel'; +import * as LanguageModel from '@effect/ai/LanguageModel'; +import * as Prompt from '@effect/ai/Prompt'; +import { Effect, Layer, Redacted } from 'effect'; + +// Create OpenRouter client layer +const OpenRouterClientLayer = OpenRouterClient.layer({ + apiKey: Redacted.make(process.env.OPENROUTER_API_KEY!), +}).pipe(Layer.provide(FetchHttpClient.layer)); + +// Create language model layer with CRITICAL stream_options config +const OpenRouterModelLayer = OpenRouterLanguageModel.layer({ + model: 'anthropic/claude-3.5-sonnet', + config: { + stream_options: { include_usage: true }, // CRITICAL: Required! + }, +}).pipe(Layer.provide(OpenRouterClientLayer)); + +// Use in Effect.gen program +const program = Effect.gen(function* () { + const response = yield* LanguageModel.generateText({ + prompt: Prompt.make([ + { + role: 'user', + content: [ + { + type: 'text', + text: 'Large context here...', + options: { + openrouter: { + cacheControl: { type: 'ephemeral' }, // Cache this block + }, + }, + }, + { + type: 'text', + text: 'Your question here', + }, + ], + }, + ]), + }); + + // Check cache metrics + const cachedTokens = response.usage.cachedInputTokens ?? 0; +}); + +// Run with dependencies +await program.pipe( + Effect.provide(OpenRouterModelLayer), + Effect.runPromise, +); +``` + +## Important Notes + +### Critical Configuration +**MUST include `stream_options: { include_usage: true }` in model config** +- Without this, usage.cachedInputTokens will be undefined +- OpenRouterClient only sets this for streaming by default +- Must be set explicitly in the layer configuration + +### Cache Metrics Location +Cache metrics are in `response.usage`: +```typescript +{ + inputTokens: number, + outputTokens: number, + cachedInputTokens: number // Number of tokens read from cache +} +``` + +### Requirements +1. **stream_options.include_usage = true** - In model config layer +2. **Minimum 2048+ tokens** - Smaller content may not be cached +3. **options.openrouter.cacheControl** - On content items in Prompt +4. **Exact match** - Cache only hits on identical content + +### Expected Behavior +- **First call**: `cachedInputTokens = 0` (cache miss, creates cache) +- **Second call**: `cachedInputTokens > 0` (cache hit, reads from cache) + +### Effect-Specific Patterns +- Use `Effect.gen` for composable effect workflows +- Layer-based dependency injection for client and model +- Type-safe error handling via Effect type +- Structured concurrency with Effect.sleep for delays + +## Scientific Method +All examples follow evidence-based verification: +- **Hypothesis**: options.openrouter.cacheControl triggers caching +- **Experiment**: Make identical calls twice +- **Evidence**: Measure via response.usage.cachedInputTokens +- **Analysis**: Compare cache miss vs cache hit diff --git a/typescript/effect-ai/src/prompt-caching/multi-message-cache.ts b/typescript/effect-ai/src/prompt-caching/multi-message-cache.ts new file mode 100644 index 0000000..d810728 --- /dev/null +++ b/typescript/effect-ai/src/prompt-caching/multi-message-cache.ts @@ -0,0 +1,115 @@ +/** + * Example: Anthropic Prompt Caching - Multi-Message Conversation (Effect AI) + * + * This example demonstrates Anthropic prompt caching in a multi-message conversation + * via OpenRouter using Effect AI. + * + * Pattern: User message cache in multi-turn conversation using Effect patterns + * + * To run: bun run typescript/effect-ai/src/prompt-caching/multi-message-cache.ts + */ + +import * as OpenRouterClient from '@effect/ai-openrouter/OpenRouterClient'; +import * as OpenRouterLanguageModel from '@effect/ai-openrouter/OpenRouterLanguageModel'; +import * as LanguageModel from '@effect/ai/LanguageModel'; +import * as Prompt from '@effect/ai/Prompt'; +import { FetchHttpClient } from '@effect/platform'; +import * as BunContext from '@effect/platform-bun/BunContext'; +import { LARGE_SYSTEM_PROMPT } from '@openrouter-examples/shared/constants'; +import { Console, Effect, Layer, Redacted } from 'effect'; + +const program = Effect.gen(function* () { + const testId = Date.now(); + const largeContext = `Test ${testId}: Context:\n\n${LARGE_SYSTEM_PROMPT}`; + + yield* Console.log('╔════════════════════════════════════════════════════════════════════════════╗'); + yield* Console.log('║ Anthropic Prompt Caching - Multi-Message (Effect AI) ║'); + yield* Console.log('╚════════════════════════════════════════════════════════════════════════════╝'); + yield* Console.log(''); + yield* Console.log('Testing cache_control in multi-turn conversation'); + yield* Console.log(''); + + const makePrompt = () => + Prompt.make([ + { + role: 'user' as const, + content: [ + { + type: 'text' as const, + text: largeContext, + options: { + openrouter: { + cacheControl: { type: 'ephemeral' as const }, + }, + }, + }, + { + type: 'text' as const, + text: "Hello, what's your purpose?", + }, + ], + }, + { + role: 'assistant' as const, + content: "I'm an AI assistant designed to help with various tasks.", + }, + { + role: 'user' as const, + content: 'What programming languages do you know?', + }, + ]); + + yield* Console.log('First Call (Cache Miss Expected)'); + const response1 = yield* LanguageModel.generateText({ + prompt: makePrompt(), + }); + const cached1 = response1.usage.cachedInputTokens ?? 0; + yield* Console.log(` Response: ${response1.text.substring(0, 80)}...`); + yield* Console.log(` cached_tokens=${cached1}`); + + yield* Effect.sleep('1 second'); + + yield* Console.log('\nSecond Call (Cache Hit Expected)'); + const response2 = yield* LanguageModel.generateText({ + prompt: makePrompt(), + }); + const cached2 = response2.usage.cachedInputTokens ?? 0; + yield* Console.log(` Response: ${response2.text.substring(0, 80)}...`); + yield* Console.log(` cached_tokens=${cached2}`); + + // Analysis + yield* Console.log('\n' + '='.repeat(80)); + yield* Console.log('ANALYSIS'); + yield* Console.log('='.repeat(80)); + yield* Console.log(`First call: cached_tokens=${cached1} (expected: 0)`); + yield* Console.log(`Second call: cached_tokens=${cached2} (expected: >0)`); + + const success = cached1 === 0 && cached2 > 0; + + if (success) { + yield* Console.log('\n✓ SUCCESS - Multi-message caching is working correctly'); + } else { + yield* Console.log('\n✗ FAILURE - Multi-message caching is not working as expected'); + } + + yield* Console.log('='.repeat(80)); +}); + +const OpenRouterClientLayer = OpenRouterClient.layer({ + apiKey: Redacted.make(process.env.OPENROUTER_API_KEY!), +}).pipe(Layer.provide(FetchHttpClient.layer)); + +const OpenRouterModelLayer = OpenRouterLanguageModel.layer({ + model: 'anthropic/claude-3.5-sonnet', + config: { + stream_options: { include_usage: true }, + }, +}).pipe(Layer.provide(OpenRouterClientLayer)); + +await program.pipe( + Effect.provide(OpenRouterModelLayer), + Effect.provide(BunContext.layer), + Effect.runPromise, +); + +console.log('\n✓ Program completed successfully'); diff --git a/typescript/effect-ai/src/prompt-caching/no-cache-control.ts b/typescript/effect-ai/src/prompt-caching/no-cache-control.ts new file mode 100644 index 0000000..184f6fe --- /dev/null +++ b/typescript/effect-ai/src/prompt-caching/no-cache-control.ts @@ -0,0 +1,107 @@ +/** + * Example: Anthropic Prompt Caching - Control (No cache_control) (Effect AI) + * + * This is a CONTROL scenario demonstrating that without cache_control, + * no caching occurs. + * + * Purpose: Validates that cache behavior is due to cache_control, not coincidence + * + * To run: bun run typescript/effect-ai/src/prompt-caching/no-cache-control.ts + */ + +import * as OpenRouterClient from '@effect/ai-openrouter/OpenRouterClient'; +import * as OpenRouterLanguageModel from '@effect/ai-openrouter/OpenRouterLanguageModel'; +import * as LanguageModel from '@effect/ai/LanguageModel'; +import * as Prompt from '@effect/ai/Prompt'; +import { FetchHttpClient } from '@effect/platform'; +import * as BunContext from '@effect/platform-bun/BunContext'; +import { LARGE_SYSTEM_PROMPT } from '@openrouter-examples/shared/constants'; +import { Console, Effect, Layer, Redacted } from 'effect'; + +const program = Effect.gen(function* () { + const testId = Date.now(); + const largeContext = `Test ${testId}: Context:\n\n${LARGE_SYSTEM_PROMPT}`; + + yield* Console.log('╔════════════════════════════════════════════════════════════════════════════╗'); + yield* Console.log('║ Anthropic Prompt Caching - Control (No cache_control) (Effect AI) ║'); + yield* Console.log('╚════════════════════════════════════════════════════════════════════════════╝'); + yield* Console.log(''); + yield* Console.log('Testing WITHOUT cache_control (control scenario)'); + yield* Console.log(''); + + const makePrompt = () => + Prompt.make([ + { + role: 'user' as const, + content: [ + { + type: 'text' as const, + text: largeContext, + // NO cache_control - this is the control + }, + { + type: 'text' as const, + text: 'What are the key principles?', + }, + ], + }, + ]); + + yield* Console.log('First Call (No Cache Expected)'); + const response1 = yield* LanguageModel.generateText({ + prompt: makePrompt(), + }); + const cached1 = response1.usage.cachedInputTokens ?? 0; + yield* Console.log(` cached_tokens=${cached1}`); + + yield* Effect.sleep('1 second'); + + yield* Console.log('\nSecond Call (No Cache Expected)'); + const response2 = yield* LanguageModel.generateText({ + prompt: makePrompt(), + }); + const cached2 = response2.usage.cachedInputTokens ?? 0; + yield* Console.log(` cached_tokens=${cached2}`); + + // Analysis + yield* Console.log('\n' + '='.repeat(80)); + yield* Console.log('ANALYSIS (CONTROL)'); + yield* Console.log('='.repeat(80)); + yield* Console.log(`First call: cached_tokens=${cached1} (expected: 0)`); + yield* Console.log(`Second call: cached_tokens=${cached2} (expected: 0)`); + + if (cached1 === 0 && cached2 === 0) { + yield* Console.log('✓ No cache metrics present (expected for control)'); + } else { + yield* Console.log('✗ Unexpected cache metrics in control scenario'); + } + + const success = cached1 === 0 && cached2 === 0; + + if (success) { + yield* Console.log('\n✓ SUCCESS - Control scenario confirms no false positives'); + } else { + yield* Console.log('\n✗ FAILURE - Control scenario shows unexpected cache behavior'); + } + + yield* Console.log('='.repeat(80)); +}); + +const OpenRouterClientLayer = OpenRouterClient.layer({ + apiKey: Redacted.make(process.env.OPENROUTER_API_KEY!), +}).pipe(Layer.provide(FetchHttpClient.layer)); + +const OpenRouterModelLayer = OpenRouterLanguageModel.layer({ + model: 'anthropic/claude-3.5-sonnet', + config: { + stream_options: { include_usage: true }, + }, +}).pipe(Layer.provide(OpenRouterClientLayer)); + +await program.pipe( + Effect.provide(OpenRouterModelLayer), + Effect.provide(BunContext.layer), + Effect.runPromise, +); + +console.log('\n✓ Program completed successfully'); diff --git a/typescript/effect-ai/src/prompt-caching/user-message-cache.ts b/typescript/effect-ai/src/prompt-caching/user-message-cache.ts new file mode 100644 index 0000000..73398fe --- /dev/null +++ b/typescript/effect-ai/src/prompt-caching/user-message-cache.ts @@ -0,0 +1,124 @@ +/** + * Example: Anthropic Prompt Caching - User Message (Effect AI) + * + * This example demonstrates Anthropic prompt caching on a user message via OpenRouter + * using @effect/ai and @effect/ai-openrouter with Effect's idiomatic patterns. + * + * Pattern: User message with options.openrouter.cacheControl in Prompt + * - Effect.gen for composable effects + * - Layer-based dependency injection + * - Cache control on content items in Prompt.make + * + * CRITICAL CONFIGURATION: + * - **MUST** include stream_options: { include_usage: true } in model config layer + * - Without it, usage.cachedInputTokens will be undefined + * + * To run: bun run typescript/effect-ai/src/prompt-caching/user-message-cache.ts + */ + +import * as OpenRouterClient from '@effect/ai-openrouter/OpenRouterClient'; +import * as OpenRouterLanguageModel from '@effect/ai-openrouter/OpenRouterLanguageModel'; +import * as LanguageModel from '@effect/ai/LanguageModel'; +import * as Prompt from '@effect/ai/Prompt'; +import { FetchHttpClient } from '@effect/platform'; +import * as BunContext from '@effect/platform-bun/BunContext'; +import { LARGE_SYSTEM_PROMPT } from '@openrouter-examples/shared/constants'; +import { Console, Effect, Layer, Redacted } from 'effect'; + +/** + * Main program + */ +const program = Effect.gen(function* () { + const testId = Date.now(); + const largeContext = `Test ${testId}: Here is a comprehensive codebase to analyze:\n\n${LARGE_SYSTEM_PROMPT}`; + + yield* Console.log('╔════════════════════════════════════════════════════════════════════════════╗'); + yield* Console.log('║ Anthropic Prompt Caching - User Message (Effect AI) ║'); + yield* Console.log('╚════════════════════════════════════════════════════════════════════════════╝'); + yield* Console.log(''); + yield* Console.log('Testing cache_control on user message content block'); + yield* Console.log(`Context size: ${largeContext.length} characters (~${Math.round(largeContext.length / 4)} tokens)`); + yield* Console.log(''); + + const makePrompt = () => + Prompt.make([ + { + role: 'user' as const, + content: [ + { + type: 'text' as const, + text: largeContext, + options: { + openrouter: { + cacheControl: { type: 'ephemeral' as const }, + }, + }, + }, + { + type: 'text' as const, + text: 'Based on this codebase, what are the main patterns used?', + }, + ], + }, + ]); + + yield* Console.log('First Call (Cache Miss Expected)'); + const response1 = yield* LanguageModel.generateText({ + prompt: makePrompt(), + }); + + yield* Console.log(` Response: ${response1.text.substring(0, 100)}...`); + yield* Console.log(` Cached tokens: ${response1.usage.cachedInputTokens ?? 0}`); + + yield* Effect.sleep('1 second'); + + yield* Console.log('\nSecond Call (Cache Hit Expected)'); + const response2 = yield* LanguageModel.generateText({ + prompt: makePrompt(), + }); + + yield* Console.log(` Response: ${response2.text.substring(0, 100)}...`); + yield* Console.log(` Cached tokens: ${response2.usage.cachedInputTokens ?? 0}`); + + // Analysis + yield* Console.log('\n' + '='.repeat(80)); + yield* Console.log('ANALYSIS'); + yield* Console.log('='.repeat(80)); + + const cached1 = response1.usage.cachedInputTokens ?? 0; + const cached2 = response2.usage.cachedInputTokens ?? 0; + + yield* Console.log(`First call: cached_tokens=${cached1} (expected: 0)`); + yield* Console.log(`Second call: cached_tokens=${cached2} (expected: >0)`); + + const success = cached1 === 0 && cached2 > 0; + + if (success) { + yield* Console.log('\n✓ SUCCESS - User message caching is working correctly'); + } else { + yield* Console.log('\n✗ FAILURE - User message caching is not working as expected'); + } + + yield* Console.log('='.repeat(80)); +}); + +// Create layers +const OpenRouterClientLayer = OpenRouterClient.layer({ + apiKey: Redacted.make(process.env.OPENROUTER_API_KEY!), +}).pipe(Layer.provide(FetchHttpClient.layer)); + +const OpenRouterModelLayer = OpenRouterLanguageModel.layer({ + model: 'anthropic/claude-3.5-sonnet', + config: { + stream_options: { include_usage: true }, // CRITICAL! + }, +}).pipe(Layer.provide(OpenRouterClientLayer)); + +// Run program +await program.pipe( + Effect.provide(OpenRouterModelLayer), + Effect.provide(BunContext.layer), + Effect.runPromise, +); + +console.log('\n✓ Program completed successfully'); diff --git a/typescript/effect-ai/tsconfig.json b/typescript/effect-ai/tsconfig.json new file mode 100644 index 0000000..822a727 --- /dev/null +++ b/typescript/effect-ai/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "lib": ["ES2022"], + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "types": ["bun-types"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} diff --git a/typescript/package.json b/typescript/package.json index 37c9484..2e0ec08 100644 --- a/typescript/package.json +++ b/typescript/package.json @@ -12,7 +12,8 @@ "workspaces": [ "shared", "fetch", - "ai-sdk-v5" + "ai-sdk-v5", + "effect-ai" ], "devDependencies": { "@types/bun": "latest", From efaa146ba96ebdd10143eb68c714aa24e5c8e4e7 Mon Sep 17 00:00:00 2001 From: Tom Aylott Date: Tue, 11 Nov 2025 18:16:47 -0500 Subject: [PATCH 2/6] Run biome format --- .../src/prompt-caching/multi-message-cache.ts | 12 +++++++++--- .../src/prompt-caching/no-cache-control.ts | 12 +++++++++--- .../src/prompt-caching/user-message-cache.ts | 16 ++++++++++++---- 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/typescript/effect-ai/src/prompt-caching/multi-message-cache.ts b/typescript/effect-ai/src/prompt-caching/multi-message-cache.ts index d810728..b6e2a24 100644 --- a/typescript/effect-ai/src/prompt-caching/multi-message-cache.ts +++ b/typescript/effect-ai/src/prompt-caching/multi-message-cache.ts @@ -22,9 +22,15 @@ const program = Effect.gen(function* () { const testId = Date.now(); const largeContext = `Test ${testId}: Context:\n\n${LARGE_SYSTEM_PROMPT}`; - yield* Console.log('╔════════════════════════════════════════════════════════════════════════════╗'); - yield* Console.log('║ Anthropic Prompt Caching - Multi-Message (Effect AI) ║'); - yield* Console.log('╚════════════════════════════════════════════════════════════════════════════╝'); + yield* Console.log( + '╔════════════════════════════════════════════════════════════════════════════╗', + ); + yield* Console.log( + '║ Anthropic Prompt Caching - Multi-Message (Effect AI) ║', + ); + yield* Console.log( + '╚════════════════════════════════════════════════════════════════════════════╝', + ); yield* Console.log(''); yield* Console.log('Testing cache_control in multi-turn conversation'); yield* Console.log(''); diff --git a/typescript/effect-ai/src/prompt-caching/no-cache-control.ts b/typescript/effect-ai/src/prompt-caching/no-cache-control.ts index 184f6fe..9e156ab 100644 --- a/typescript/effect-ai/src/prompt-caching/no-cache-control.ts +++ b/typescript/effect-ai/src/prompt-caching/no-cache-control.ts @@ -22,9 +22,15 @@ const program = Effect.gen(function* () { const testId = Date.now(); const largeContext = `Test ${testId}: Context:\n\n${LARGE_SYSTEM_PROMPT}`; - yield* Console.log('╔════════════════════════════════════════════════════════════════════════════╗'); - yield* Console.log('║ Anthropic Prompt Caching - Control (No cache_control) (Effect AI) ║'); - yield* Console.log('╚════════════════════════════════════════════════════════════════════════════╝'); + yield* Console.log( + '╔════════════════════════════════════════════════════════════════════════════╗', + ); + yield* Console.log( + '║ Anthropic Prompt Caching - Control (No cache_control) (Effect AI) ║', + ); + yield* Console.log( + '╚════════════════════════════════════════════════════════════════════════════╝', + ); yield* Console.log(''); yield* Console.log('Testing WITHOUT cache_control (control scenario)'); yield* Console.log(''); diff --git a/typescript/effect-ai/src/prompt-caching/user-message-cache.ts b/typescript/effect-ai/src/prompt-caching/user-message-cache.ts index 73398fe..ddd7fb9 100644 --- a/typescript/effect-ai/src/prompt-caching/user-message-cache.ts +++ b/typescript/effect-ai/src/prompt-caching/user-message-cache.ts @@ -32,12 +32,20 @@ const program = Effect.gen(function* () { const testId = Date.now(); const largeContext = `Test ${testId}: Here is a comprehensive codebase to analyze:\n\n${LARGE_SYSTEM_PROMPT}`; - yield* Console.log('╔════════════════════════════════════════════════════════════════════════════╗'); - yield* Console.log('║ Anthropic Prompt Caching - User Message (Effect AI) ║'); - yield* Console.log('╚════════════════════════════════════════════════════════════════════════════╝'); + yield* Console.log( + '╔════════════════════════════════════════════════════════════════════════════╗', + ); + yield* Console.log( + '║ Anthropic Prompt Caching - User Message (Effect AI) ║', + ); + yield* Console.log( + '╚════════════════════════════════════════════════════════════════════════════╝', + ); yield* Console.log(''); yield* Console.log('Testing cache_control on user message content block'); - yield* Console.log(`Context size: ${largeContext.length} characters (~${Math.round(largeContext.length / 4)} tokens)`); + yield* Console.log( + `Context size: ${largeContext.length} characters (~${Math.round(largeContext.length / 4)} tokens)`, + ); yield* Console.log(''); const makePrompt = () => From b4b9011c5d86381295e7a6211bb3ff88b7468e19 Mon Sep 17 00:00:00 2001 From: Tom Aylott Date: Tue, 11 Nov 2025 18:41:10 -0500 Subject: [PATCH 3/6] Simplify Effect AI prompt-caching README to link to main docs --- .../effect-ai/src/prompt-caching/README.md | 123 +++++------------- 1 file changed, 30 insertions(+), 93 deletions(-) diff --git a/typescript/effect-ai/src/prompt-caching/README.md b/typescript/effect-ai/src/prompt-caching/README.md index 02725f6..df31c40 100644 --- a/typescript/effect-ai/src/prompt-caching/README.md +++ b/typescript/effect-ai/src/prompt-caching/README.md @@ -1,121 +1,58 @@ -# Anthropic Prompt Caching Examples (Effect AI) +# Prompt Caching Examples (Effect AI) -This directory contains examples demonstrating Anthropic's prompt caching feature via OpenRouter using @effect/ai and @effect/ai-openrouter. +Examples demonstrating prompt caching with @effect/ai and @effect/ai-openrouter. -## What is Prompt Caching? +## Documentation -Anthropic's prompt caching allows you to cache large portions of your prompts to: -- **Reduce costs** - Cached tokens cost significantly less -- **Improve latency** - Cached content is processed faster -- **Enable larger contexts** - Use more context without proportional cost increases +For full prompt caching documentation including all providers, pricing, and configuration details, see: +- **[Prompt Caching Guide](../../../../docs/prompt-caching.md)** -Cache TTL: 5 minutes for ephemeral caches +## Examples in This Directory -## Examples +- `user-message-cache.ts` - Cache large context in user messages +- `multi-message-cache.ts` - Cache system prompt across multi-turn conversations +- `no-cache-control.ts` - Control scenario (validates methodology) + +## Quick Start -### User Message Cache (`user-message-cache.ts`) -Cache large context in user messages using Effect AI: ```bash +# Run an example bun run typescript/effect-ai/src/prompt-caching/user-message-cache.ts ``` -**Pattern**: User message with `options.openrouter.cacheControl` using Effect.gen - -## How to Use with Effect AI +## Effect AI Usage ```typescript -import * as OpenRouterClient from '@effect/ai-openrouter/OpenRouterClient'; import * as OpenRouterLanguageModel from '@effect/ai-openrouter/OpenRouterLanguageModel'; -import * as LanguageModel from '@effect/ai/LanguageModel'; -import * as Prompt from '@effect/ai/Prompt'; -import { Effect, Layer, Redacted } from 'effect'; - -// Create OpenRouter client layer -const OpenRouterClientLayer = OpenRouterClient.layer({ - apiKey: Redacted.make(process.env.OPENROUTER_API_KEY!), -}).pipe(Layer.provide(FetchHttpClient.layer)); -// Create language model layer with CRITICAL stream_options config const OpenRouterModelLayer = OpenRouterLanguageModel.layer({ model: 'anthropic/claude-3.5-sonnet', config: { - stream_options: { include_usage: true }, // CRITICAL: Required! + stream_options: { include_usage: true }, // Required for cache metrics }, -}).pipe(Layer.provide(OpenRouterClientLayer)); +}); -// Use in Effect.gen program const program = Effect.gen(function* () { const response = yield* LanguageModel.generateText({ - prompt: Prompt.make([ - { - role: 'user', - content: [ - { - type: 'text', - text: 'Large context here...', - options: { - openrouter: { - cacheControl: { type: 'ephemeral' }, // Cache this block - }, - }, - }, - { - type: 'text', - text: 'Your question here', - }, - ], - }, - ]), + prompt: Prompt.make([{ + role: 'user', + content: [{ + type: 'text', + text: 'Large context...', + options: { + openrouter: { cacheControl: { type: 'ephemeral' } } + } + }] + }]) }); // Check cache metrics - const cachedTokens = response.usage.cachedInputTokens ?? 0; + const cached = response.usage.cachedInputTokens ?? 0; }); - -// Run with dependencies -await program.pipe( - Effect.provide(OpenRouterModelLayer), - Effect.runPromise, -); -``` - -## Important Notes - -### Critical Configuration -**MUST include `stream_options: { include_usage: true }` in model config** -- Without this, usage.cachedInputTokens will be undefined -- OpenRouterClient only sets this for streaming by default -- Must be set explicitly in the layer configuration - -### Cache Metrics Location -Cache metrics are in `response.usage`: -```typescript -{ - inputTokens: number, - outputTokens: number, - cachedInputTokens: number // Number of tokens read from cache -} ``` -### Requirements -1. **stream_options.include_usage = true** - In model config layer -2. **Minimum 2048+ tokens** - Smaller content may not be cached -3. **options.openrouter.cacheControl** - On content items in Prompt -4. **Exact match** - Cache only hits on identical content - -### Expected Behavior -- **First call**: `cachedInputTokens = 0` (cache miss, creates cache) -- **Second call**: `cachedInputTokens > 0` (cache hit, reads from cache) - -### Effect-Specific Patterns -- Use `Effect.gen` for composable effect workflows -- Layer-based dependency injection for client and model -- Type-safe error handling via Effect type -- Structured concurrency with Effect.sleep for delays +## Effect-Specific Notes -## Scientific Method -All examples follow evidence-based verification: -- **Hypothesis**: options.openrouter.cacheControl triggers caching -- **Experiment**: Make identical calls twice -- **Evidence**: Measure via response.usage.cachedInputTokens -- **Analysis**: Compare cache miss vs cache hit +- Use layer-based dependency injection for client and model configuration +- `stream_options.include_usage` must be set in the model layer config +- Cache metrics appear in `response.usage.cachedInputTokens` From a847da9f7bb2f8f35bbda5d4d057bdbedec3a4d2 Mon Sep 17 00:00:00 2001 From: Tom Aylott Date: Tue, 11 Nov 2025 18:48:03 -0500 Subject: [PATCH 4/6] Rename prompt caching examples with anthropic prefix --- .../{multi-message-cache.ts => anthropic-multi-message-cache.ts} | 0 .../{no-cache-control.ts => anthropic-no-cache-control.ts} | 0 .../{user-message-cache.ts => anthropic-user-message-cache.ts} | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename typescript/effect-ai/src/prompt-caching/{multi-message-cache.ts => anthropic-multi-message-cache.ts} (100%) rename typescript/effect-ai/src/prompt-caching/{no-cache-control.ts => anthropic-no-cache-control.ts} (100%) rename typescript/effect-ai/src/prompt-caching/{user-message-cache.ts => anthropic-user-message-cache.ts} (100%) diff --git a/typescript/effect-ai/src/prompt-caching/multi-message-cache.ts b/typescript/effect-ai/src/prompt-caching/anthropic-multi-message-cache.ts similarity index 100% rename from typescript/effect-ai/src/prompt-caching/multi-message-cache.ts rename to typescript/effect-ai/src/prompt-caching/anthropic-multi-message-cache.ts diff --git a/typescript/effect-ai/src/prompt-caching/no-cache-control.ts b/typescript/effect-ai/src/prompt-caching/anthropic-no-cache-control.ts similarity index 100% rename from typescript/effect-ai/src/prompt-caching/no-cache-control.ts rename to typescript/effect-ai/src/prompt-caching/anthropic-no-cache-control.ts diff --git a/typescript/effect-ai/src/prompt-caching/user-message-cache.ts b/typescript/effect-ai/src/prompt-caching/anthropic-user-message-cache.ts similarity index 100% rename from typescript/effect-ai/src/prompt-caching/user-message-cache.ts rename to typescript/effect-ai/src/prompt-caching/anthropic-user-message-cache.ts From 284043b8736f0cae3dcec38b5fffd2471b8eb2f9 Mon Sep 17 00:00:00 2001 From: Tom Aylott Date: Wed, 12 Nov 2025 17:28:27 -0500 Subject: [PATCH 5/6] Use glob pattern runner for effect-ai examples MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace hardcoded filename list with run-examples.ts that auto-discovers all .ts files in src/ Benefits: - Add new example → automatically included in 'bun examples' - Rename example → no package.json update needed - Impossible for package.json to reference non-existent files Also fixes stale filenames (user-message-cache.ts → anthropic-user-message-cache.ts) --- typescript/effect-ai/package.json | 2 +- typescript/effect-ai/run-examples.ts | 57 ++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) create mode 100755 typescript/effect-ai/run-examples.ts diff --git a/typescript/effect-ai/package.json b/typescript/effect-ai/package.json index b485208..22efafd 100644 --- a/typescript/effect-ai/package.json +++ b/typescript/effect-ai/package.json @@ -4,7 +4,7 @@ "private": true, "type": "module", "scripts": { - "examples": "bun run src/prompt-caching/user-message-cache.ts && bun run src/prompt-caching/multi-message-cache.ts && bun run src/prompt-caching/no-cache-control.ts", + "examples": "bun run run-examples.ts", "typecheck": "tsc --noEmit" }, "dependencies": { diff --git a/typescript/effect-ai/run-examples.ts b/typescript/effect-ai/run-examples.ts new file mode 100755 index 0000000..67429a3 --- /dev/null +++ b/typescript/effect-ai/run-examples.ts @@ -0,0 +1,57 @@ +#!/usr/bin/env bun +/** + * Run all example files in the src/ directory + * Each example is run in a separate process to handle process.exit() calls + */ + +import { readdirSync, statSync } from 'fs'; +import { join } from 'path'; +import { $ } from 'bun'; + +const srcDir = join(import.meta.dir, 'src'); + +// Recursively find all .ts files in src/ +function findExamples(dir: string): string[] { + const entries = readdirSync(dir); + const files: string[] = []; + + for (const entry of entries) { + const fullPath = join(dir, entry); + const stat = statSync(fullPath); + + if (stat.isDirectory()) { + files.push(...findExamples(fullPath)); + } else if (entry.endsWith('.ts')) { + files.push(fullPath); + } + } + + return files.sort(); +} + +const examples = findExamples(srcDir); +console.log(`Found ${examples.length} example(s)\n`); + +let failed = 0; +for (const example of examples) { + const relativePath = example.replace(import.meta.dir + '/', ''); + console.log(`\n${'='.repeat(80)}`); + console.log(`Running: ${relativePath}`); + console.log('='.repeat(80)); + + try { + await $`bun run ${example}`.quiet(); + console.log(`✅ ${relativePath} completed successfully`); + } catch (error) { + console.error(`❌ ${relativePath} failed`); + failed++; + } +} + +console.log(`\n${'='.repeat(80)}`); +console.log(`Results: ${examples.length - failed}/${examples.length} passed`); +console.log('='.repeat(80)); + +if (failed > 0) { + process.exit(1); +} From 25cbe9812aa004dcc964a68d130406f1534b9cc1 Mon Sep 17 00:00:00 2001 From: Tom Aylott Date: Wed, 12 Nov 2025 18:05:41 -0500 Subject: [PATCH 6/6] Remove filename references from docs to prevent sync issues --- typescript/effect-ai/README.md | 2 +- typescript/effect-ai/src/prompt-caching/README.md | 11 +---------- .../prompt-caching/anthropic-multi-message-cache.ts | 2 -- .../src/prompt-caching/anthropic-no-cache-control.ts | 2 -- .../prompt-caching/anthropic-user-message-cache.ts | 2 -- 5 files changed, 2 insertions(+), 17 deletions(-) diff --git a/typescript/effect-ai/README.md b/typescript/effect-ai/README.md index f847c90..c30a5da 100644 --- a/typescript/effect-ai/README.md +++ b/typescript/effect-ai/README.md @@ -20,7 +20,7 @@ bun examples ## Features -- [prompt-caching.ts](./src/prompt-caching.ts) - Anthropic caching with Effect patterns +- [prompt-caching](./src/prompt-caching/) - Anthropic caching examples with Effect patterns ### Key Configuration diff --git a/typescript/effect-ai/src/prompt-caching/README.md b/typescript/effect-ai/src/prompt-caching/README.md index df31c40..dcf1e68 100644 --- a/typescript/effect-ai/src/prompt-caching/README.md +++ b/typescript/effect-ai/src/prompt-caching/README.md @@ -9,16 +9,7 @@ For full prompt caching documentation including all providers, pricing, and conf ## Examples in This Directory -- `user-message-cache.ts` - Cache large context in user messages -- `multi-message-cache.ts` - Cache system prompt across multi-turn conversations -- `no-cache-control.ts` - Control scenario (validates methodology) - -## Quick Start - -```bash -# Run an example -bun run typescript/effect-ai/src/prompt-caching/user-message-cache.ts -``` +See the TypeScript files in this directory for specific examples. ## Effect AI Usage diff --git a/typescript/effect-ai/src/prompt-caching/anthropic-multi-message-cache.ts b/typescript/effect-ai/src/prompt-caching/anthropic-multi-message-cache.ts index b6e2a24..b2e94c5 100644 --- a/typescript/effect-ai/src/prompt-caching/anthropic-multi-message-cache.ts +++ b/typescript/effect-ai/src/prompt-caching/anthropic-multi-message-cache.ts @@ -5,8 +5,6 @@ * via OpenRouter using Effect AI. * * Pattern: User message cache in multi-turn conversation using Effect patterns - * - * To run: bun run typescript/effect-ai/src/prompt-caching/multi-message-cache.ts */ import * as OpenRouterClient from '@effect/ai-openrouter/OpenRouterClient'; diff --git a/typescript/effect-ai/src/prompt-caching/anthropic-no-cache-control.ts b/typescript/effect-ai/src/prompt-caching/anthropic-no-cache-control.ts index 9e156ab..9d97acb 100644 --- a/typescript/effect-ai/src/prompt-caching/anthropic-no-cache-control.ts +++ b/typescript/effect-ai/src/prompt-caching/anthropic-no-cache-control.ts @@ -5,8 +5,6 @@ * no caching occurs. * * Purpose: Validates that cache behavior is due to cache_control, not coincidence - * - * To run: bun run typescript/effect-ai/src/prompt-caching/no-cache-control.ts */ import * as OpenRouterClient from '@effect/ai-openrouter/OpenRouterClient'; diff --git a/typescript/effect-ai/src/prompt-caching/anthropic-user-message-cache.ts b/typescript/effect-ai/src/prompt-caching/anthropic-user-message-cache.ts index ddd7fb9..7126ef2 100644 --- a/typescript/effect-ai/src/prompt-caching/anthropic-user-message-cache.ts +++ b/typescript/effect-ai/src/prompt-caching/anthropic-user-message-cache.ts @@ -12,8 +12,6 @@ * CRITICAL CONFIGURATION: * - **MUST** include stream_options: { include_usage: true } in model config layer * - Without it, usage.cachedInputTokens will be undefined - * - * To run: bun run typescript/effect-ai/src/prompt-caching/user-message-cache.ts */ import * as OpenRouterClient from '@effect/ai-openrouter/OpenRouterClient';